feat: improve realtime messaging and media handling
This commit is contained in:
Vendored
+219
-55
@@ -6,12 +6,21 @@ header('Cache-Control: no-store');
|
||||
|
||||
require __DIR__ . '/im_web_bootstrap.php';
|
||||
|
||||
const WEB_MEDIA_ROOT = '/opt/im-server/media';
|
||||
define(
|
||||
'WEB_MEDIA_ROOT',
|
||||
webImEnv(
|
||||
'JC_WEB_IM_MEDIA_ROOT',
|
||||
is_dir('/var/www/im/public/media')
|
||||
? '/var/www/im/public/media'
|
||||
: (is_dir('/var/www/IM/public/media') ? '/var/www/IM/public/media' : '/opt/im-server/media')
|
||||
)
|
||||
);
|
||||
define('WEB_MEDIA_PUBLIC_PORT', (int)webImEnv('JC_WEB_IM_MEDIA_PORT', '80'));
|
||||
const WEB_MEDIA_TYPE_IMAGE = 1;
|
||||
const WEB_MEDIA_TYPE_FILE = 4;
|
||||
const WEB_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
||||
const WEB_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
||||
const WEB_MINIO_PROJECT_CODE = '001';
|
||||
|
||||
function isDailyMuteActive(string $dailyStart, string $dailyEnd, int $nowMs): bool
|
||||
{
|
||||
@@ -72,6 +81,33 @@ function isGroupMutedForSender(PDO $appDb, string $groupId, string $role, int $n
|
||||
return $startsAtMs > 0 && $endsAtMs > $startsAtMs && $nowMs >= $startsAtMs && $nowMs < $endsAtMs;
|
||||
}
|
||||
|
||||
function nativeGroupRoleFromType(mixed $type): string
|
||||
{
|
||||
$value = (int)$type;
|
||||
if ($value === 2) {
|
||||
return 'owner';
|
||||
}
|
||||
if ($value === 1) {
|
||||
return 'admin';
|
||||
}
|
||||
|
||||
return 'member';
|
||||
}
|
||||
|
||||
function fetchNativeGroupForSender(PDO $db, string $sender, string $groupId): array
|
||||
{
|
||||
$statement = $db->prepare(
|
||||
'select g.`_gid`, g.`_name`, g.`_mute`, g.`_deleted`, m.`_type` as `member_type`'
|
||||
. ' from `t_group` g'
|
||||
. ' join `t_group_member` m on m.`_gid` = g.`_gid`'
|
||||
. ' where g.`_gid` = ? and g.`_deleted` = 0 and m.`_mid` = ? limit 1'
|
||||
);
|
||||
$statement->execute([$groupId, $sender]);
|
||||
$group = $statement->fetch();
|
||||
|
||||
return is_array($group) ? $group : [];
|
||||
}
|
||||
|
||||
function messageRequestValue(array $payload, string $key, string $default = ''): string
|
||||
{
|
||||
return trim((string)($_POST[$key] ?? $payload[$key] ?? $default));
|
||||
@@ -162,7 +198,106 @@ function mediaPublicBaseUrl(): string
|
||||
return $scheme . '://' . $host . ':' . $port;
|
||||
}
|
||||
|
||||
function saveMessageAttachmentFile(array $file, string $attachmentType): array
|
||||
function messageStorageConfig(): array
|
||||
{
|
||||
$config = ['enabled' => false, 'url' => ''];
|
||||
foreach (['t_app_system_conf', 'system_conf'] as $table) {
|
||||
try {
|
||||
$statement = imAppDb()->query(
|
||||
'select `conf_name`, `conf_value` from `' . $table . '`'
|
||||
. " where lower(`conf_name`) in ('is_minioplus', 'minio_url') and `is_valid` = 1"
|
||||
);
|
||||
$rows = $statement->fetchAll();
|
||||
if (!$rows) {
|
||||
continue;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
$name = strtolower((string)($row['conf_name'] ?? ''));
|
||||
if ($name === 'is_minioplus') {
|
||||
$config['enabled'] = (string)($row['conf_value'] ?? '0') === '1';
|
||||
} elseif ($name === 'minio_url') {
|
||||
$config['url'] = rtrim(trim((string)($row['conf_value'] ?? '')), '/');
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch (Throwable $error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
function saveMessageAttachmentToMinio(
|
||||
array $file,
|
||||
string $attachmentType,
|
||||
string $sender,
|
||||
string $baseUrl
|
||||
): array {
|
||||
$size = (int)($file['size'] ?? 0);
|
||||
$tmpName = (string)($file['tmp_name'] ?? '');
|
||||
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
|
||||
throw new RuntimeException('文件上传失败');
|
||||
}
|
||||
$mime = detectedUploadMime($tmpName);
|
||||
if ($attachmentType === 'image') {
|
||||
$mime = normalizedImageUploadMime($tmpName, $mime);
|
||||
}
|
||||
$originalName = safeUploadName((string)($file['name'] ?? ($attachmentType === 'image' ? 'image.jpg' : 'file')));
|
||||
$content = file_get_contents($tmpName);
|
||||
if ($content === false) {
|
||||
throw new RuntimeException('读取上传文件失败');
|
||||
}
|
||||
$requestBody = json_encode([
|
||||
'projectCode' => WEB_MINIO_PROJECT_CODE,
|
||||
'fileScene' => 'temp',
|
||||
'fileName' => $originalName,
|
||||
'contentType' => $mime,
|
||||
'base64Content' => base64_encode($content),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($requestBody)) {
|
||||
throw new RuntimeException('生成图片上传请求失败');
|
||||
}
|
||||
|
||||
$handle = curl_init($baseUrl . '/project-storage/upload');
|
||||
curl_setopt_array($handle, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 8,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: minio-' . WEB_MINIO_PROJECT_CODE . '-' . $sender,
|
||||
],
|
||||
CURLOPT_POSTFIELDS => $requestBody,
|
||||
]);
|
||||
$responseBody = curl_exec($handle);
|
||||
$status = (int)curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
|
||||
$curlError = curl_error($handle);
|
||||
curl_close($handle);
|
||||
$response = is_string($responseBody) ? json_decode($responseBody, true) : null;
|
||||
$data = is_array($response) && is_array($response['data'] ?? null) ? $response['data'] : [];
|
||||
$url = trim((string)($data['previewUrl'] ?? $data['downloadUrl'] ?? ''));
|
||||
if ($status < 200 || $status >= 300 || (int)($response['code'] ?? -1) !== 0 || $url === '') {
|
||||
$message = trim((string)($response['message'] ?? ''));
|
||||
throw new RuntimeException(
|
||||
$message !== ''
|
||||
? 'MinIO上传失败:' . $message
|
||||
: 'MinIO上传失败' . ($curlError !== '' ? ':' . $curlError : '')
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $attachmentType,
|
||||
'url' => $url,
|
||||
'name' => $originalName,
|
||||
'mime' => $mime,
|
||||
'size' => $size,
|
||||
'mediaType' => $attachmentType === 'image' ? WEB_MEDIA_TYPE_IMAGE : WEB_MEDIA_TYPE_FILE,
|
||||
];
|
||||
}
|
||||
|
||||
function saveMessageAttachmentFile(array $file, string $attachmentType, string $sender): array
|
||||
{
|
||||
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) {
|
||||
throw new RuntimeException('文件上传失败');
|
||||
@@ -172,10 +307,6 @@ function saveMessageAttachmentFile(array $file, string $attachmentType): array
|
||||
if ($size <= 0 || $size > $maxSize) {
|
||||
throw new RuntimeException($attachmentType === 'image' ? '图片不能超过10MB' : '文件不能超过50MB');
|
||||
}
|
||||
if (!is_dir(WEB_MEDIA_ROOT) || !is_writable(WEB_MEDIA_ROOT)) {
|
||||
throw new RuntimeException('媒体存储目录不可写');
|
||||
}
|
||||
|
||||
$tmpName = (string)($file['tmp_name'] ?? '');
|
||||
if ($tmpName === '' || !is_uploaded_file($tmpName)) {
|
||||
throw new RuntimeException('文件上传失败');
|
||||
@@ -185,6 +316,17 @@ function saveMessageAttachmentFile(array $file, string $attachmentType): array
|
||||
$mime = normalizedImageUploadMime($tmpName, $mime);
|
||||
}
|
||||
|
||||
$storage = messageStorageConfig();
|
||||
if ($storage['enabled']) {
|
||||
if ($storage['url'] === '') {
|
||||
throw new RuntimeException('MinIO已开启但未配置服务地址');
|
||||
}
|
||||
return saveMessageAttachmentToMinio($file, $attachmentType, $sender, $storage['url']);
|
||||
}
|
||||
if (!is_dir(WEB_MEDIA_ROOT) || !is_writable(WEB_MEDIA_ROOT)) {
|
||||
throw new RuntimeException('媒体存储目录不可写');
|
||||
}
|
||||
|
||||
$mediaType = $attachmentType === 'image' ? WEB_MEDIA_TYPE_IMAGE : WEB_MEDIA_TYPE_FILE;
|
||||
$datePath = 'fs/' . $mediaType . '/' . date('Y/m/d/H/i/s');
|
||||
$targetDir = rtrim(WEB_MEDIA_ROOT, '/') . '/' . $datePath;
|
||||
@@ -249,7 +391,7 @@ function storeMessageAttachment(
|
||||
]);
|
||||
}
|
||||
|
||||
function attachmentFromRequest(string $messageType): ?array
|
||||
function attachmentFromRequest(string $messageType, string $sender): ?array
|
||||
{
|
||||
if (!in_array($messageType, ['image', 'file'], true)) {
|
||||
return null;
|
||||
@@ -260,7 +402,61 @@ function attachmentFromRequest(string $messageType): ?array
|
||||
respond(400, '缺少上传文件');
|
||||
}
|
||||
|
||||
return saveMessageAttachmentFile($attachmentFile, $messageType);
|
||||
return saveMessageAttachmentFile($attachmentFile, $messageType, $sender);
|
||||
}
|
||||
|
||||
function messagePayloadBody(string $text, ?array $attachment): array
|
||||
{
|
||||
$pushContent = $text !== ''
|
||||
? $text
|
||||
: ($attachment
|
||||
? ((string)$attachment['type'] === 'image' ? '[图片]' : '[文件] ' . (string)$attachment['name'])
|
||||
: $text);
|
||||
$payloadBody = [
|
||||
'type' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : 1,
|
||||
'searchableContent' => $pushContent,
|
||||
'pushContent' => $pushContent,
|
||||
'content' => $text,
|
||||
'persistFlag' => 3,
|
||||
];
|
||||
if ($attachment) {
|
||||
$payloadBody['mediaType'] = (int)$attachment['mediaType'];
|
||||
$payloadBody['remoteMediaUrl'] = (string)$attachment['url'];
|
||||
$payloadBody['extra'] = json_encode([
|
||||
'name' => (string)$attachment['name'],
|
||||
'mime' => (string)$attachment['mime'],
|
||||
'size' => (int)$attachment['size'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return $payloadBody;
|
||||
}
|
||||
|
||||
function sendNativeGroupMessage(string $sender, string $target, string $text, ?array $attachment): array
|
||||
{
|
||||
$result = requireImSuccess(imAdminPost('/admin/message/send', [
|
||||
'sender' => $sender,
|
||||
'conv' => [
|
||||
'type' => 1,
|
||||
'target' => $target,
|
||||
'line' => 0,
|
||||
],
|
||||
'payload' => messagePayloadBody($text, $attachment),
|
||||
'userMessage' => true,
|
||||
]), '原生群消息发送失败');
|
||||
|
||||
$messageUid = isset($result['messageUid']) ? (string)$result['messageUid'] : '';
|
||||
$timestamp = (int)($result['timestamp'] ?? webImNowMs());
|
||||
if ($attachment && $messageUid !== '') {
|
||||
ensureWebImAppTables();
|
||||
storeMessageAttachment(imAppDb(), $messageUid, 'group', $target, $sender, $attachment, $timestamp);
|
||||
}
|
||||
|
||||
return [
|
||||
'messageUid' => $messageUid !== '' ? $messageUid : null,
|
||||
'timestamp' => $timestamp,
|
||||
'attachment' => $attachment,
|
||||
];
|
||||
}
|
||||
|
||||
$contentTypeHeader = (string)($_SERVER['CONTENT_TYPE'] ?? '');
|
||||
@@ -292,7 +488,17 @@ try {
|
||||
$memberStatement->execute([$target, $sender]);
|
||||
$group = $memberStatement->fetch();
|
||||
if (!$group) {
|
||||
respond(403, '你不在该群组中');
|
||||
$nativeGroup = fetchNativeGroupForSender(imDb(), $sender, $target);
|
||||
if (!$nativeGroup) {
|
||||
respond(403, '你不在该群组中');
|
||||
}
|
||||
$attachment = attachmentFromRequest($messageType, $sender);
|
||||
$nativeResult = sendNativeGroupMessage($sender, $target, $text, $attachment);
|
||||
respond(0, 'success', [
|
||||
...$nativeResult,
|
||||
'senderRole' => nativeGroupRoleFromType($nativeGroup['member_type'] ?? 0),
|
||||
'native' => true,
|
||||
]);
|
||||
}
|
||||
$category = (string)($group['category'] ?? 'group');
|
||||
$role = (string)($group['role'] ?? 'member');
|
||||
@@ -304,7 +510,7 @@ try {
|
||||
if ($category !== 'channel' && isGroupMutedForSender($appDb, $target, $role, $now)) {
|
||||
respond(403, '当前群组处于禁言时段');
|
||||
}
|
||||
$attachment = attachmentFromRequest($messageType);
|
||||
$attachment = attachmentFromRequest($messageType, $sender);
|
||||
$messageUid = webImMessageUid();
|
||||
$insert = $appDb->prepare(
|
||||
'insert into `t_web_group_messages`'
|
||||
@@ -320,27 +526,6 @@ try {
|
||||
|
||||
$nativeError = '';
|
||||
try {
|
||||
$pushContent = $text !== ''
|
||||
? $text
|
||||
: ($attachment
|
||||
? ((string)$attachment['type'] === 'image' ? '[图片]' : '[文件] ' . (string)$attachment['name'])
|
||||
: $text);
|
||||
$payloadBody = [
|
||||
'type' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : 1,
|
||||
'searchableContent' => $pushContent,
|
||||
'pushContent' => $pushContent,
|
||||
'content' => $text,
|
||||
'persistFlag' => 3,
|
||||
];
|
||||
if ($attachment) {
|
||||
$payloadBody['mediaType'] = (int)$attachment['mediaType'];
|
||||
$payloadBody['remoteMediaUrl'] = (string)$attachment['url'];
|
||||
$payloadBody['extra'] = json_encode([
|
||||
'name' => (string)$attachment['name'],
|
||||
'mime' => (string)$attachment['mime'],
|
||||
'size' => (int)$attachment['size'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
requireImSuccess(imAdminPost('/admin/message/send', [
|
||||
'sender' => $sender,
|
||||
'conv' => [
|
||||
@@ -348,7 +533,7 @@ try {
|
||||
'target' => $target,
|
||||
'line' => 0,
|
||||
],
|
||||
'payload' => $payloadBody,
|
||||
'payload' => messagePayloadBody($text, $attachment),
|
||||
'userMessage' => true,
|
||||
]), '原生群消息发送失败');
|
||||
} catch (Throwable $error) {
|
||||
@@ -364,28 +549,7 @@ try {
|
||||
]);
|
||||
}
|
||||
|
||||
$attachment = attachmentFromRequest($messageType);
|
||||
$pushContent = $text !== ''
|
||||
? $text
|
||||
: ($attachment
|
||||
? ((string)$attachment['type'] === 'image' ? '[图片]' : '[文件] ' . (string)$attachment['name'])
|
||||
: $text);
|
||||
$payloadBody = [
|
||||
'type' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : 1,
|
||||
'searchableContent' => $pushContent,
|
||||
'pushContent' => $pushContent,
|
||||
'content' => $text,
|
||||
'persistFlag' => 3,
|
||||
];
|
||||
if ($attachment) {
|
||||
$payloadBody['mediaType'] = (int)$attachment['mediaType'];
|
||||
$payloadBody['remoteMediaUrl'] = (string)$attachment['url'];
|
||||
$payloadBody['extra'] = json_encode([
|
||||
'name' => (string)$attachment['name'],
|
||||
'mime' => (string)$attachment['mime'],
|
||||
'size' => (int)$attachment['size'],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$attachment = attachmentFromRequest($messageType, $sender);
|
||||
|
||||
$result = requireImSuccess(imAdminPost('/admin/message/send', [
|
||||
'sender' => $sender,
|
||||
@@ -394,7 +558,7 @@ try {
|
||||
'target' => $target,
|
||||
'line' => 0,
|
||||
],
|
||||
'payload' => $payloadBody,
|
||||
'payload' => messagePayloadBody($text, $attachment),
|
||||
'userMessage' => true,
|
||||
]), '发送失败');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user