// // LNIMMessageData.swift // Lanu // // Created by OneeChan on 2025/12/10. // import Foundation import AutoCodable enum LNIMMessageDataType { case none case official case system case text case image case voice case order case call(String) case autoReply(LNAutoReplyType) var isUserContent: Bool { switch self { case .text, .image, .voice: true default: false } } } enum LNIMMessageCustomType: String, Decodable { case none case official_image_text case playmate_order case playmate_welcome_text case playmate_welcome_voice } @AutoCodable class LNIMCustomMessage: Decodable { var businessID: LNIMMessageCustomType = .none } extension SignalingActionType: @retroactive Decodable { } @AutoCodable class LNIMVoiceCallMessage: Decodable { var inviter: String = "" var actionType: SignalingActionType? var businessID: Int = 0 var inviteID: String = "" var data: String = "" private var decodedData: LNIMVoiceCallData? var callData: LNIMVoiceCallData? { if let decodedData { return decodedData } guard let data = data.data(using: .utf8) else { return nil } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys decodedData = try? decoder.decode(LNIMVoiceCallData.self, from: data) return decodedData } var contentDesc: String { guard let callData else { return "" } return if actionType == SignalingActionType.invite, let data = callData.data, data.cmd == "hangup" { .init(key: "C00001") + " " + callData.call_end.timeCountDisplay } else if actionType == SignalingActionType.cancel_Invite { if inviter.isMyUid { .init(key: "A00152") } else { .init(key: "A00142") } } else if actionType == SignalingActionType.reject_Invite { if callData.data?.cmd == "line_busy" { if inviter.isMyUid { .init(key: "C00004") } else { .init(key: "C00011") } } else { if inviter.isMyUid { .init(key: "C00005") } else { .init(key: "C00009") } } } else if actionType == SignalingActionType.invite_Timeout { if inviter.isMyUid { .init(key: "C00010") } else { .init(key: "C00002") } } else { "" } } } @AutoCodable class LNIMVoiceCallData: Decodable { var call_end: Int = 0 var data: LNIMVoiceCallExtraData? init() { } } @AutoCodable class LNIMVoiceCallExtraData: Decodable { var excludeFromHistoryMessage: Bool = false var cmd: String = "" } @AutoCodable class LNIMOfficialMessage: Decodable { var title: String = "" var content: String = "" var image: String = "" var link: String = "" } @AutoCodable class LNIMOrderMessage: Decodable, LNOrderProtocol { var orderId: String = "" var bizCategoryName: String = "" var categoryIcon: String = "" var price: Double = 0.0 var unit: String = "" var purchaseQty: Int = 0 var status: LNOrderStatus = .created var createTime: Double = 0.0 var star: Double = 0.0 var refundApply: Bool = false var customerRemark: String = "" var buyerUserNo: String = "" } @AutoCodable class LNAutoReplyTextMessage: Decodable { var textContent: String = "" } @AutoCodable class LNAutoReplyVoiceMessage: Decodable { var voiceUrl: String = "" var voiceDuration: Int = 0 } private extension V2TIMElemType { var toDataType: LNIMMessageDataType { switch self { case .ELEM_TYPE_TEXT: .text case .ELEM_TYPE_IMAGE: .image case .ELEM_TYPE_SOUND: .voice case .ELEM_TYPE_CUSTOM: .none default: .none } } } class LNIMMessageData: NSObject { let imMessage: V2TIMMessage private(set) var type: LNIMMessageDataType private(set) var content: String? private(set) var textContent: NSAttributedString? private(set) var voiceDuration: Int = 0 private var customMessage: (any Decodable)? var readReceipt: V2TIMMessageReceipt? var isSelf: Bool init(imMessage: V2TIMMessage) { self.imMessage = imMessage self.type = imMessage.elemType.toDataType self.isSelf = imMessage.isSelf super.init() switch imMessage.elemType { case .ELEM_TYPE_TEXT: setupTextContent() case .ELEM_TYPE_IMAGE: setupImageContent() case .ELEM_TYPE_SOUND: setupVoiceContent() case .ELEM_TYPE_CUSTOM: setupCustomContent() default: break } } func decodeCustomMessage() -> T? { if let cached = customMessage as? T { return cached } guard let data = imMessage.customElem?.data else { return nil } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys let result = try? decoder.decode(T.self, from: data) if let result { customMessage = result } return result } static func buildDateMessage(date: Date) -> LNIMMessageData { let data = LNIMMessageData(imMessage: V2TIMMessage()) data.type = .system data.content = date.timeIntervalSince1970.tencentIMTimeDesc return data } } extension LNIMMessageData { private func setupTextContent() { content = imMessage.textElem?.text textContent = content?.getEmojiString(with: .body_l) } private func setupImageContent() { if imMessage.isSelf, let path = imMessage.imageElem?.path { if FileManager.default.fileExists(atPath: path) { content = path // 使用本地文件路径 return } else if let fixedPath = path.fixedFilePath, FileManager.default.fileExists(atPath: fixedPath) { content = fixedPath // UUID 变化后的本地文件路径 return } } content = imMessage.imageElem?.imageList.first(where: { $0.type == .IMAGE_TYPE_THUMB })?.url } private func setupVoiceContent() { content = imMessage.soundElem?.path voiceDuration = Int(imMessage.soundElem?.duration ?? 0) } private func setupCustomContent() { guard let data = imMessage.customElem?.data else { return } let decoder = JSONDecoder() decoder.keyDecodingStrategy = .useDefaultKeys // 处理蛇形命名 // 判断是否是通话消息 if let voiceCall = try? decoder.decode(LNIMVoiceCallMessage.self, from: data), voiceCall.actionType != nil, voiceCall.businessID != 0 { type = .call(voiceCall.inviteID) return } // 判断是否是自定义消息 if let result = try? decoder.decode(LNIMCustomMessage.self, from: data) { switch result.businessID { case .playmate_order: type = .order // 订单消息 case .official_image_text: type = .official // 官方消息 case .playmate_welcome_text: type = .autoReply(.text) if let message: LNAutoReplyTextMessage = decodeCustomMessage() { textContent = NSMutableAttributedString(string: message.textContent) } case .playmate_welcome_voice: type = .autoReply(.voice) if let message: LNAutoReplyVoiceMessage = decodeCustomMessage() { content = message.voiceUrl voiceDuration = message.voiceDuration } case .none: type = .none } return } type = .none } }