fix: 加强 IM 上传 URL 与 RTC 来电载荷校验
parent
309a4bf4d0
commit
8b06efe5ee
|
|
@ -1,6 +1,7 @@
|
|||
import { updateFile } from '@/api/infra/file'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useMessage } from '@/hooks/web/useMessage'
|
||||
import { isOpenableUrl } from '@/utils/url'
|
||||
|
||||
import { useConversationStore } from '../store/conversationStore'
|
||||
import { useMessageSender } from './useMessageSender'
|
||||
|
|
@ -351,6 +352,12 @@ export const useMediaUploader = () => {
|
|||
markMediaFailed(conversation.type, conversation.targetId, 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
|
||||
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 { useUserStore } from '@/store/modules/user'
|
||||
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 { isOpenableUrl } from '@/utils/url'
|
||||
import { getConversationKey } from '@/views/im/utils/conversation'
|
||||
import { ImConversationType, ImGroupMemberRole, ImMessageType } from '@/views/im/utils/constants'
|
||||
import { DANGEROUS_FILE_EXTENSIONS, MESSAGE_GROUP_READ_ENABLED } from '@/views/im/utils/config'
|
||||
|
|
@ -209,6 +213,7 @@ const {
|
|||
verifyMediaUploadStillAllowed,
|
||||
requireMediaHandler
|
||||
} = useMediaUploader()
|
||||
const muteOverlay = useMuteOverlay() // 禁言 / 封禁覆盖层
|
||||
|
||||
const editorRef = useTemplateRef<HTMLDivElement>('editorRef')
|
||||
const imageInputRef = useTemplateRef<HTMLInputElement>('imageInputRef')
|
||||
|
|
@ -244,6 +249,14 @@ function syncEditorState() {
|
|||
syncDraftToStore(editor)
|
||||
}
|
||||
|
||||
/** 禁言状态变化时同步发送按钮 */
|
||||
watch(muteOverlay, () => {
|
||||
const editor = editorRef.value
|
||||
if (editor) {
|
||||
applyEditorUiState(editor)
|
||||
}
|
||||
})
|
||||
|
||||
/** 把 editor 当前内容写到 draftStore;plain 由 collectFromEditor 拿,与发送时同源避免列表与实发不一致 */
|
||||
function syncDraftToStore(editor: HTMLDivElement) {
|
||||
const conversation = conversationStore.activeConversation
|
||||
|
|
@ -614,11 +627,6 @@ const isGroup = computed(
|
|||
() => conversationStore.activeConversation?.type === ImConversationType.GROUP
|
||||
)
|
||||
|
||||
// ==================== 禁言 / 封禁状态 ====================
|
||||
|
||||
/** 禁言 / 封禁覆盖层;handleResend 重试 / uploadAndSendMedia 上传完后也共用同一份,避免绕过 overlay */
|
||||
const muteOverlay = useMuteOverlay()
|
||||
|
||||
/** 从 groupStore 读当前激活群的成员(切会话时由 MessagePanel 预拉) */
|
||||
const groupMembers = computed<GroupMemberLite[]>(() => {
|
||||
const conversation = conversationStore.activeConversation
|
||||
|
|
@ -1106,8 +1114,20 @@ async function uploadAndSendVideo(file: File) {
|
|||
markMediaFailed(conversation.type, conversation.targetId, clientMessageId)
|
||||
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 同一道)
|
||||
if (!verifyMediaUploadStillAllowed(conversation, startKey, ImMessageType.VIDEO, clientMessageId)) {
|
||||
if (
|
||||
!verifyMediaUploadStillAllowed(conversation, startKey, ImMessageType.VIDEO, clientMessageId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1116,7 +1136,7 @@ async function uploadAndSendVideo(file: File) {
|
|||
withQuotePayload(
|
||||
videoHandler.build(file, url, {
|
||||
videoProbe: { duration: probe.duration, width: probe.width, height: probe.height },
|
||||
videoCoverUrl: coverUrl
|
||||
videoCoverUrl: safeCoverUrl
|
||||
}),
|
||||
replyQuote
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ImMessageStatus,
|
||||
ImMessageType,
|
||||
ImConversationType,
|
||||
ImRtcCallMediaType,
|
||||
ImRtcParticipantStatus,
|
||||
isFriendChatTip,
|
||||
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)
|
||||
* 不写发送人名字段:渲染层走 utils/user 实时算(备注 / 群昵称变更后历史消息自动刷新)
|
||||
|
|
@ -889,6 +919,10 @@ export const useImWebSocketStore = defineStore('imWebSocketStore', {
|
|||
}
|
||||
switch (payload.status) {
|
||||
case ImRtcParticipantStatus.INVITING:
|
||||
if (!isValidRtcInvitePayload(payload)) {
|
||||
console.warn('[IM WS] RTC_CALL invite payload 不合法', payload)
|
||||
return
|
||||
}
|
||||
// 当前已在通话中:忽略新来电;后端层面也会拒绝,这里是兜底
|
||||
if (!rtcStore.isActive) {
|
||||
rtcStore.showIncoming(payload)
|
||||
|
|
|
|||
Loading…
Reference in New Issue