fix: 加强 IM 上传 URL 与 RTC 来电载荷校验
parent
309a4bf4d0
commit
8b06efe5ee
|
|
@ -1,6 +1,7 @@
|
||||||
import { updateFile } from '@/api/infra/file'
|
import { updateFile } from '@/api/infra/file'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
|
import { isOpenableUrl } from '@/utils/url'
|
||||||
|
|
||||||
import { useConversationStore } from '../store/conversationStore'
|
import { useConversationStore } from '../store/conversationStore'
|
||||||
import { useMessageSender } from './useMessageSender'
|
import { useMessageSender } from './useMessageSender'
|
||||||
|
|
@ -351,6 +352,12 @@ export const useMediaUploader = () => {
|
||||||
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
||||||
return clientMessageId
|
return clientMessageId
|
||||||
}
|
}
|
||||||
|
if (!isOpenableUrl(url)) {
|
||||||
|
console.warn(`[IM] ${handler.kind}上传返回了不支持打开的 URL`, { url })
|
||||||
|
message.warning('上传返回的文件地址不支持打开')
|
||||||
|
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
||||||
|
return clientMessageId
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 上传期间会话切换 / 用户登出 / 被禁言:任一情况都放弃发送,占位置 FAILED
|
// 3. 上传期间会话切换 / 用户登出 / 被禁言:任一情况都放弃发送,占位置 FAILED
|
||||||
if (!verifyMediaUploadStillAllowed(conversation, startKey, opts.type, clientMessageId)) {
|
if (!verifyMediaUploadStillAllowed(conversation, startKey, opts.type, clientMessageId)) {
|
||||||
|
|
|
||||||
|
|
@ -172,8 +172,12 @@ import { getMemberDisplayName } from '@/views/im/utils/user'
|
||||||
import { useMessage } from '@/hooks/web/useMessage'
|
import { useMessage } from '@/hooks/web/useMessage'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
import { useMessageSender } from '@/views/im/home/composables/useMessageSender'
|
import { useMessageSender } from '@/views/im/home/composables/useMessageSender'
|
||||||
import { ensureMediaSizeWithinLimit, useMediaUploader } from '@/views/im/home/composables/useMediaUploader'
|
import {
|
||||||
|
ensureMediaSizeWithinLimit,
|
||||||
|
useMediaUploader
|
||||||
|
} from '@/views/im/home/composables/useMediaUploader'
|
||||||
import { useMuteOverlay } from '@/views/im/home/composables/useMuteOverlay'
|
import { useMuteOverlay } from '@/views/im/home/composables/useMuteOverlay'
|
||||||
|
import { isOpenableUrl } from '@/utils/url'
|
||||||
import { getConversationKey } from '@/views/im/utils/conversation'
|
import { getConversationKey } from '@/views/im/utils/conversation'
|
||||||
import { ImConversationType, ImGroupMemberRole, ImMessageType } from '@/views/im/utils/constants'
|
import { ImConversationType, ImGroupMemberRole, ImMessageType } from '@/views/im/utils/constants'
|
||||||
import { DANGEROUS_FILE_EXTENSIONS, MESSAGE_GROUP_READ_ENABLED } from '@/views/im/utils/config'
|
import { DANGEROUS_FILE_EXTENSIONS, MESSAGE_GROUP_READ_ENABLED } from '@/views/im/utils/config'
|
||||||
|
|
@ -209,6 +213,7 @@ const {
|
||||||
verifyMediaUploadStillAllowed,
|
verifyMediaUploadStillAllowed,
|
||||||
requireMediaHandler
|
requireMediaHandler
|
||||||
} = useMediaUploader()
|
} = useMediaUploader()
|
||||||
|
const muteOverlay = useMuteOverlay() // 禁言 / 封禁覆盖层
|
||||||
|
|
||||||
const editorRef = useTemplateRef<HTMLDivElement>('editorRef')
|
const editorRef = useTemplateRef<HTMLDivElement>('editorRef')
|
||||||
const imageInputRef = useTemplateRef<HTMLInputElement>('imageInputRef')
|
const imageInputRef = useTemplateRef<HTMLInputElement>('imageInputRef')
|
||||||
|
|
@ -244,6 +249,14 @@ function syncEditorState() {
|
||||||
syncDraftToStore(editor)
|
syncDraftToStore(editor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 禁言状态变化时同步发送按钮 */
|
||||||
|
watch(muteOverlay, () => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
if (editor) {
|
||||||
|
applyEditorUiState(editor)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/** 把 editor 当前内容写到 draftStore;plain 由 collectFromEditor 拿,与发送时同源避免列表与实发不一致 */
|
/** 把 editor 当前内容写到 draftStore;plain 由 collectFromEditor 拿,与发送时同源避免列表与实发不一致 */
|
||||||
function syncDraftToStore(editor: HTMLDivElement) {
|
function syncDraftToStore(editor: HTMLDivElement) {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
|
|
@ -614,11 +627,6 @@ const isGroup = computed(
|
||||||
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
||||||
)
|
)
|
||||||
|
|
||||||
// ==================== 禁言 / 封禁状态 ====================
|
|
||||||
|
|
||||||
/** 禁言 / 封禁覆盖层;handleResend 重试 / uploadAndSendMedia 上传完后也共用同一份,避免绕过 overlay */
|
|
||||||
const muteOverlay = useMuteOverlay()
|
|
||||||
|
|
||||||
/** 从 groupStore 读当前激活群的成员(切会话时由 MessagePanel 预拉) */
|
/** 从 groupStore 读当前激活群的成员(切会话时由 MessagePanel 预拉) */
|
||||||
const groupMembers = computed<GroupMemberLite[]>(() => {
|
const groupMembers = computed<GroupMemberLite[]>(() => {
|
||||||
const conversation = conversationStore.activeConversation
|
const conversation = conversationStore.activeConversation
|
||||||
|
|
@ -1106,8 +1114,20 @@ async function uploadAndSendVideo(file: File) {
|
||||||
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!isOpenableUrl(url)) {
|
||||||
|
console.warn('[IM] 视频上传返回了不支持打开的 URL', { url })
|
||||||
|
message.warning('上传返回的视频地址不支持打开')
|
||||||
|
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const safeCoverUrl = coverUrl && isOpenableUrl(coverUrl) ? coverUrl : undefined
|
||||||
|
if (coverUrl && !safeCoverUrl) {
|
||||||
|
console.warn('[IM] 视频封面上传返回了不支持打开的 URL', { coverUrl })
|
||||||
|
}
|
||||||
// 3.3 上传后会话校验 + muteOverlay 复查(与 useMediaUploader.uploadAndSendMedia 同一道)
|
// 3.3 上传后会话校验 + muteOverlay 复查(与 useMediaUploader.uploadAndSendMedia 同一道)
|
||||||
if (!verifyMediaUploadStillAllowed(conversation, startKey, ImMessageType.VIDEO, clientMessageId)) {
|
if (
|
||||||
|
!verifyMediaUploadStillAllowed(conversation, startKey, ImMessageType.VIDEO, clientMessageId)
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1116,7 +1136,7 @@ async function uploadAndSendVideo(file: File) {
|
||||||
withQuotePayload(
|
withQuotePayload(
|
||||||
videoHandler.build(file, url, {
|
videoHandler.build(file, url, {
|
||||||
videoProbe: { duration: probe.duration, width: probe.width, height: probe.height },
|
videoProbe: { duration: probe.duration, width: probe.width, height: probe.height },
|
||||||
videoCoverUrl: coverUrl
|
videoCoverUrl: safeCoverUrl
|
||||||
}),
|
}),
|
||||||
replyQuote
|
replyQuote
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ImMessageStatus,
|
ImMessageStatus,
|
||||||
ImMessageType,
|
ImMessageType,
|
||||||
ImConversationType,
|
ImConversationType,
|
||||||
|
ImRtcCallMediaType,
|
||||||
ImRtcParticipantStatus,
|
ImRtcParticipantStatus,
|
||||||
isFriendChatTip,
|
isFriendChatTip,
|
||||||
isFriendNotification,
|
isFriendNotification,
|
||||||
|
|
@ -63,6 +64,35 @@ const isFriendDeleteWithClear = (frame: ImPrivateMessageDTO): boolean => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RTC_LIVEKIT_PROTOCOLS = new Set(['ws:', 'wss:', 'http:', 'https:'])
|
||||||
|
const RTC_MEDIA_TYPES = new Set<number>(Object.values(ImRtcCallMediaType))
|
||||||
|
|
||||||
|
/** 校验 LiveKit 连接地址 */
|
||||||
|
function isValidLiveKitUrl(url?: string): boolean {
|
||||||
|
if (!url) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return RTC_LIVEKIT_PROTOCOLS.has(new URL(url).protocol)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验来电信令载荷 */
|
||||||
|
function isValidRtcInvitePayload(payload: ImRtcCallNotification): boolean {
|
||||||
|
if (!payload.room || !payload.token || !isValidLiveKitUrl(payload.livekitUrl)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!RTC_MEDIA_TYPES.has(payload.mediaType) || !payload.inviterUserId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (payload.conversationType === ImConversationType.PRIVATE) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return payload.conversationType === ImConversationType.GROUP && !!payload.groupId
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebSocket 私聊 DTO -> 前端 Message;targetId 是会话主键(对端 userId)
|
* WebSocket 私聊 DTO -> 前端 Message;targetId 是会话主键(对端 userId)
|
||||||
* 不写发送人名字段:渲染层走 utils/user 实时算(备注 / 群昵称变更后历史消息自动刷新)
|
* 不写发送人名字段:渲染层走 utils/user 实时算(备注 / 群昵称变更后历史消息自动刷新)
|
||||||
|
|
@ -889,6 +919,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
||||||
}
|
}
|
||||||
switch (payload.status) {
|
switch (payload.status) {
|
||||||
case ImRtcParticipantStatus.INVITING:
|
case ImRtcParticipantStatus.INVITING:
|
||||||
|
if (!isValidRtcInvitePayload(payload)) {
|
||||||
|
console.warn('[IM WS] RTC_CALL invite payload 不合法', payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
// 当前已在通话中:忽略新来电;后端层面也会拒绝,这里是兜底
|
// 当前已在通话中:忽略新来电;后端层面也会拒绝,这里是兜底
|
||||||
if (!rtcStore.isActive) {
|
if (!rtcStore.isActive) {
|
||||||
rtcStore.showIncoming(payload)
|
rtcStore.showIncoming(payload)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue