LNIMMessageData.swift 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. //
  2. // LNIMMessageData.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/10.
  6. //
  7. import Foundation
  8. import AutoCodable
  9. enum LNIMMessageDataType {
  10. case none
  11. case official
  12. case system
  13. case text
  14. case image
  15. case voice
  16. case order
  17. case call(String)
  18. case autoReply(LNAutoReplyType)
  19. }
  20. enum LNIMMessageCustomType: String, Decodable {
  21. case none
  22. case official_image_text
  23. case playmate_order
  24. case playmate_welcome_text
  25. case playmate_welcome_voice
  26. }
  27. @AutoCodable
  28. class LNIMCustomMessage: Decodable {
  29. var businessID: LNIMMessageCustomType = .none
  30. }
  31. extension SignalingActionType: @retroactive Decodable { }
  32. @AutoCodable
  33. class LNIMVoiceCallMessage: Decodable {
  34. var inviter: String = ""
  35. var actionType: SignalingActionType?
  36. var businessID: Int = 0
  37. var inviteID: String = ""
  38. var data: String = ""
  39. private var decodedData: LNIMVoiceCallData?
  40. var callData: LNIMVoiceCallData? {
  41. if let decodedData {
  42. return decodedData
  43. }
  44. guard let data = data.data(using: .utf8) else { return nil }
  45. let decoder = JSONDecoder()
  46. decoder.keyDecodingStrategy = .useDefaultKeys
  47. decodedData = try? decoder.decode(LNIMVoiceCallData.self, from: data)
  48. return decodedData
  49. }
  50. var contentDesc: String {
  51. guard let callData else { return "" }
  52. return if actionType == SignalingActionType.invite,
  53. let data = callData.data, data.cmd == "hangup" {
  54. .init(key: "C00001") + " " + callData.call_end.timeCountDisplay
  55. } else if actionType == SignalingActionType.cancel_Invite {
  56. if inviter.isMyUid {
  57. .init(key: "A00152")
  58. } else {
  59. .init(key: "A00142")
  60. }
  61. } else if actionType == SignalingActionType.reject_Invite {
  62. if callData.data?.cmd == "line_busy" {
  63. if inviter.isMyUid {
  64. .init(key: "C00004")
  65. } else {
  66. .init(key: "C00011")
  67. }
  68. } else {
  69. if inviter.isMyUid {
  70. .init(key: "C00005")
  71. } else {
  72. .init(key: "C00009")
  73. }
  74. }
  75. } else if actionType == SignalingActionType.invite_Timeout {
  76. if inviter.isMyUid {
  77. .init(key: "C00010")
  78. } else {
  79. .init(key: "C00002")
  80. }
  81. } else {
  82. ""
  83. }
  84. }
  85. }
  86. @AutoCodable
  87. class LNIMVoiceCallData: Decodable {
  88. var call_end: Int = 0
  89. var data: LNIMVoiceCallExtraData?
  90. init() { }
  91. }
  92. @AutoCodable
  93. class LNIMVoiceCallExtraData: Decodable {
  94. var excludeFromHistoryMessage: Bool = false
  95. var cmd: String = ""
  96. }
  97. @AutoCodable
  98. class LNIMOfficialMessage: Decodable {
  99. var title: String = ""
  100. var content: String = ""
  101. var image: String = ""
  102. var link: String = ""
  103. }
  104. @AutoCodable
  105. class LNIMOrderMessage: Decodable, LNOrderProtocol {
  106. var orderId: String = ""
  107. var bizCategoryName: String = ""
  108. var categoryIcon: String = ""
  109. var price: Double = 0.0
  110. var unit: String = ""
  111. var purchaseQty: Int = 0
  112. var status: LNOrderStatus = .created
  113. var createTime: Double = 0.0
  114. var star: Double = 0.0
  115. var refundApply: Bool = false
  116. var customerRemark: String = ""
  117. var buyerUserNo: String = ""
  118. }
  119. @AutoCodable
  120. class LNAutoReplyTextMessage: Decodable {
  121. var textContent: String = ""
  122. }
  123. @AutoCodable
  124. class LNAutoReplyVoiceMessage: Decodable {
  125. var voiceUrl: String = ""
  126. var voiceDuration: Int = 0
  127. }
  128. private extension V2TIMElemType {
  129. var toDataType: LNIMMessageDataType {
  130. switch self {
  131. case .ELEM_TYPE_TEXT: .text
  132. case .ELEM_TYPE_IMAGE: .image
  133. case .ELEM_TYPE_SOUND: .voice
  134. case .ELEM_TYPE_CUSTOM: .none
  135. default: .none
  136. }
  137. }
  138. }
  139. class LNIMMessageData: NSObject {
  140. let imMessage: V2TIMMessage
  141. private(set) var type: LNIMMessageDataType
  142. private(set) var content: String?
  143. private(set) var textContent: NSAttributedString?
  144. private(set) var voiceDuration: Int = 0
  145. private var customMessage: (any Decodable)?
  146. var readReceipt: V2TIMMessageReceipt?
  147. var isSelf: Bool
  148. init(imMessage: V2TIMMessage) {
  149. self.imMessage = imMessage
  150. self.type = imMessage.elemType.toDataType
  151. self.isSelf = imMessage.isSelf
  152. super.init()
  153. switch imMessage.elemType {
  154. case .ELEM_TYPE_TEXT: setupTextContent()
  155. case .ELEM_TYPE_IMAGE: setupImageContent()
  156. case .ELEM_TYPE_SOUND: setupVoiceContent()
  157. case .ELEM_TYPE_CUSTOM: setupCustomContent()
  158. default:
  159. break
  160. }
  161. }
  162. func decodeCustomMessage<T: Decodable>() -> T? {
  163. if let cached = customMessage as? T {
  164. return cached
  165. }
  166. guard let data = imMessage.customElem?.data else { return nil }
  167. let decoder = JSONDecoder()
  168. decoder.keyDecodingStrategy = .useDefaultKeys
  169. let result = try? decoder.decode(T.self, from: data)
  170. if let result {
  171. customMessage = result
  172. }
  173. return result
  174. }
  175. static func buildDateMessage(date: Date) -> LNIMMessageData {
  176. let data = LNIMMessageData(imMessage: V2TIMMessage())
  177. data.type = .system
  178. data.content = date.timeIntervalSince1970.tencentIMTimeDesc
  179. return data
  180. }
  181. }
  182. extension LNIMMessageData {
  183. private func setupTextContent() {
  184. content = imMessage.textElem?.text
  185. textContent = content?.getEmojiString(with: .body_l)
  186. }
  187. private func setupImageContent() {
  188. if imMessage.isSelf, let path = imMessage.imageElem?.path {
  189. if FileManager.default.fileExists(atPath: path) {
  190. content = path // 使用本地文件路径
  191. return
  192. } else if let fixedPath = path.fixedFilePath,
  193. FileManager.default.fileExists(atPath: fixedPath) {
  194. content = fixedPath // UUID 变化后的本地文件路径
  195. return
  196. }
  197. }
  198. content = imMessage.imageElem?.imageList.first(where: { $0.type == .IMAGE_TYPE_THUMB })?.url
  199. }
  200. private func setupVoiceContent() {
  201. content = imMessage.soundElem?.path
  202. voiceDuration = Int(imMessage.soundElem?.duration ?? 0)
  203. }
  204. private func setupCustomContent() {
  205. guard let data = imMessage.customElem?.data else { return }
  206. let decoder = JSONDecoder()
  207. decoder.keyDecodingStrategy = .useDefaultKeys // 处理蛇形命名
  208. // 判断是否是通话消息
  209. if let voiceCall = try? decoder.decode(LNIMVoiceCallMessage.self, from: data),
  210. voiceCall.actionType != nil, voiceCall.businessID != 0 {
  211. type = .call(voiceCall.inviteID)
  212. return
  213. }
  214. // 判断是否是自定义消息
  215. if let result = try? decoder.decode(LNIMCustomMessage.self, from: data) {
  216. switch result.businessID {
  217. case .playmate_order:
  218. type = .order // 订单消息
  219. case .official_image_text:
  220. type = .official // 官方消息
  221. case .playmate_welcome_text:
  222. type = .autoReply(.text)
  223. if let message: LNAutoReplyTextMessage = decodeCustomMessage() {
  224. textContent = NSMutableAttributedString(string: message.textContent)
  225. }
  226. case .playmate_welcome_voice:
  227. type = .autoReply(.voice)
  228. if let message: LNAutoReplyVoiceMessage = decodeCustomMessage() {
  229. content = message.voiceUrl
  230. voiceDuration = message.voiceDuration
  231. }
  232. case .none: type = .none
  233. }
  234. return
  235. }
  236. type = .none
  237. }
  238. }