// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2018-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import UIKit import ThreemaFramework class WebMessageObject: NSObject { var baseMessage: BaseMessage var type: String var id: String var body: String? var thumbnail: Dictionary? var date: Int var events: [[String: Any]]? var sortKey: UInt64 var partnerId: String? var isOutbox: Bool var isStatus: Bool = false var caption: String? var statusType: String? var unread: Bool? var state: String? var quote: Dictionary? var file: Dictionary? var video: Dictionary? var audio: Dictionary? var location: Dictionary? var voip: Dictionary? init(message:BaseMessage, conversation: Conversation, forConversationsRequest: Bool, session: WCSession) { baseMessage = message type = conversation.isGroup() ? "group" : "contact" id = message.id.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) let currentDate = message.dateForCurrentState() ?? message.date date = Int((currentDate?.timeIntervalSince1970)!) let messageEvents = MessageEvents.init(baseMessage: baseMessage) events = messageEvents.events sortKey = UInt64((message.date.timeIntervalSince1970 * 1000.0).rounded()) if message.sender != nil { if message.isOwn.boolValue { partnerId = MyIdentityStore.shared().identity } else { partnerId = message.sender.identity } } else if conversation.contact != nil { if message.isOwn.boolValue { partnerId = MyIdentityStore.shared().identity } else { partnerId = conversation.contact.identity } } isOutbox = message.isOwn.boolValue unread = !message.read.boolValue let messageState = message.messageState switch messageState { case MESSAGE_STATE_SENDING: state = "sending" break case MESSAGE_STATE_SENT: state = "sent" break case MESSAGE_STATE_DELIVERED: state = "delivered" break case MESSAGE_STATE_READ: state = "read" break case MESSAGE_STATE_USER_ACK: state = "user-ack" break case MESSAGE_STATE_USER_DECLINED: state = "user-dec" break case MESSAGE_STATE_FAILED: state = "send-failed" break default: state = nil } super.init() switch message { case is TextMessage: addTextMessage(message) case is ImageMessage: addImageMessage(message, forConversationsRequest: forConversationsRequest, session: session) case is VideoMessage: addVideoMessage(message, forConversationsRequest: forConversationsRequest, session: session) case is AudioMessage: addAudioMessage(message) case is LocationMessage: addLocationMessage(message) case is FileMessage: addFileMessage(message, forConversationsRequest: forConversationsRequest, session: session) case is SystemMessage: addSystemMessage(message) case is BallotMessage: addBallotMessage(message) default: break } } init(message:BaseMessage, conversation: Conversation) { baseMessage = message if conversation.isGroup() { type = "group" } else { type = "contact" } id = message.id.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0)) var currentDate = message.dateForCurrentState() if currentDate == nil { currentDate = message.date } date = Int((currentDate?.timeIntervalSince1970)!) sortKey = UInt64((currentDate?.timeIntervalSince1970)!) isOutbox = message.isOwn.boolValue if message is SystemMessage { let systemMessage = message as! SystemMessage if !systemMessage.isCallType() { isStatus = true } } } func objectDict() -> [String: Any] { var objectDict:[String: Any] = ["type": type, "id": id, "date": date, "isOutbox": isOutbox, "isStatus": isStatus, "sortKey": sortKey] if events != nil { objectDict.updateValue(events!, forKey: "events") } if body != nil { objectDict.updateValue(body!, forKey: "body") } if thumbnail != nil { objectDict.updateValue(thumbnail!, forKey: "thumbnail") } if partnerId != nil { objectDict.updateValue(partnerId!, forKey: "partnerId") } if caption != nil { objectDict.updateValue(caption!, forKey: "caption") } if statusType != nil { objectDict.updateValue(statusType!, forKey: "statusType") } if unread != nil { objectDict.updateValue(unread!, forKey: "unread") } if state != nil { objectDict.updateValue(state!, forKey: "state") } if quote != nil { objectDict.updateValue(quote!, forKey: "quote") } if file != nil { objectDict.updateValue(file!, forKey: "file") } if video != nil { objectDict.updateValue(video!, forKey: "video") } if audio != nil { objectDict.updateValue(audio!, forKey: "audio") } if location != nil { objectDict.updateValue(location!, forKey: "location") } if voip != nil { objectDict.updateValue(voip!, forKey: "voip") } return objectDict } func removedObjectDict() -> [String: Any] { return ["type": type, "id": id, "date": date, "isOutbox": isOutbox, "isStatus": isStatus, "sortKey": sortKey] } private func addTextMessage(_ message: BaseMessage) { let textMessage = message as! TextMessage type = "text" body = textMessage.text thumbnail = nil caption = nil statusType = "text" var quotedIdentity: NSString? var remainingBody: NSString? let quotedText = QuoteParser.parseQuote(fromMessage: textMessage.text, quotedIdentity: "edIdentity, remainingBody: &remainingBody) if quotedText != nil { quote = ["identity": quotedIdentity!, "text": quotedText!] body = remainingBody as String? } } private func addImageMessage(_ message: BaseMessage, forConversationsRequest: Bool, session: WCSession) { let imageMessage = message as! ImageMessage type = "image" body = nil statusType = "text" if imageMessage.image == nil && imageMessage.thumbnail == nil { return } else if imageMessage.image == nil { if imageMessage.thumbnail.data == nil { return } } else if imageMessage.thumbnail == nil { if imageMessage.image.data == nil { return } } else if imageMessage.image.data == nil && imageMessage.thumbnail.data == nil { return } if !session.requestedThumbnails(contains: baseMessage.id) { let webThumbnail = WebThumbnail.init(imageMessage: imageMessage, onlyThumbnail: true) thumbnail = webThumbnail.objectDict() if !forConversationsRequest { session.addRequestedThumbnail(messageId: baseMessage.id) } } if let image = imageMessage.image, let imageCaption = image.getCaption() { caption = imageCaption } } private func addVideoMessage(_ message: BaseMessage, forConversationsRequest: Bool, session: WCSession) { let videoMessage = message as! VideoMessage type = "video" body = nil if (videoMessage.thumbnail != nil), !session.requestedThumbnails(contains: baseMessage.id) { let webThumbnail = WebThumbnail.init(videoMessage, onlyThumbnail: true) thumbnail = webThumbnail.objectDict() if !forConversationsRequest { session.addRequestedThumbnail(messageId: baseMessage.id) } } caption = nil statusType = "text" let webVideo = WebVideo.init(videoMessage) video = webVideo.objectDict() } private func addAudioMessage(_ message: BaseMessage) { let audioMessage = message as! AudioMessage type = "audio" body = nil thumbnail = nil caption = nil statusType = "text" let webAudio = WebAudio.init(audioMessage) audio = webAudio.objectDict() } private func addLocationMessage(_ message: BaseMessage) { let locationMessage = message as! LocationMessage type = "location" body = nil thumbnail = nil caption = nil statusType = "text" let webLocation = WebLocation.init(locationMessage) location = webLocation.objectDict() } private func addFileMessage(_ message: BaseMessage, forConversationsRequest: Bool, session: WCSession) { let fileMessage = message as! FileMessage body = nil thumbnail = nil if let fileThumbnail = fileMessage.thumbnail, fileThumbnail.data != nil { if fileMessage.blobThumbnailId != nil { if !session.requestedThumbnails(contains: fileMessage.blobThumbnailId) { let webThumbnail = WebThumbnail.init(fileMessage, onlyThumbnail: true) thumbnail = webThumbnail.objectDict() if !forConversationsRequest { session.addRequestedThumbnail(messageId: fileMessage.blobThumbnailId) } } } else { let webThumbnail = WebThumbnail.init(fileMessage, onlyThumbnail: true) thumbnail = webThumbnail.objectDict() } } if let fileCaption = fileMessage.getCaption() { caption = fileCaption } statusType = "text" if fileMessage.renderFileImageMessage() { type = "image" } else if fileMessage.renderFileVideoMessage() { type = "video" let webVideo = WebVideo.init(fileMessage) video = webVideo.objectDict() } else { type = "file" let webFile = WebFile.init(fileMessage) file = webFile.objectDict() } } private func addSystemMessage(_ message: BaseMessage) { let systemMessage = message as! SystemMessage if systemMessage.isCallType() { // voip type = "voipStatus" body = nil thumbnail = nil caption = nil state = nil statusType = "text" if systemMessage.arg != nil { do { let argDict = try JSONSerialization.jsonObject(with: systemMessage.arg, options: .allowFragments) as! [AnyHashable: Any] isOutbox = argDict["CallInitiator"] as! Bool } catch { isOutbox = false } } else { isOutbox = true } let webVoip = WebVoip.init(systemMessage) voip = webVoip.objectDict() } else { type = "text" body = systemMessage.format() thumbnail = nil isStatus = true caption = nil statusType = "text" } } private func addBallotMessage(_ message: BaseMessage) { type = "ballot" isStatus = false statusType = "text" } } struct MessageEvents { var events = [[String: Any]]() init(baseMessage: BaseMessage) { if baseMessage.isOwn.boolValue == true { if baseMessage.remoteSentDate != nil { var event = [String: Any]() event.updateValue("sent", forKey: "type") event.updateValue(Int((baseMessage.remoteSentDate.timeIntervalSince1970)), forKey: "date") events.append(event) } if baseMessage.deliveryDate != nil { var event = [String: Any]() event.updateValue("delivered", forKey: "type") event.updateValue(Int((baseMessage.deliveryDate.timeIntervalSince1970)), forKey: "date") events.append(event) } if baseMessage.readDate != nil { var event = [String: Any]() event.updateValue("read", forKey: "type") event.updateValue(Int((baseMessage.readDate.timeIntervalSince1970)), forKey: "date") events.append(event) } } else { if baseMessage.date != nil { var event = [String: Any]() event.updateValue("sent", forKey: "type") event.updateValue(Int((baseMessage.remoteSentDate.timeIntervalSince1970)), forKey: "date") events.append(event) } if baseMessage.deliveryDate != nil { var event = [String: Any]() event.updateValue("delivered", forKey: "type") event.updateValue(Int((baseMessage.deliveryDate.timeIntervalSince1970)), forKey: "date") events.append(event) } if baseMessage.readDate != nil { var event = [String: Any]() event.updateValue("read", forKey: "type") event.updateValue(Int((baseMessage.readDate.timeIntervalSince1970)), forKey: "date") events.append(event) } if baseMessage.userackDate != nil { var event = [String: Any]() event.updateValue("acked", forKey: "type") event.updateValue(Int((baseMessage.userackDate.timeIntervalSince1970)), forKey: "date") events.append(event) } } } } struct WebBlob { var blob: Data? var type: String var name: String init(imageMessage: ImageMessage) { if imageMessage.image.data != nil { blob = imageMessage.image.data } name = imageMessage.blobGetWebFilename() type = "image/\(MEDIA_EXTENSION_IMAGE)" } init(videoMessage: VideoMessage) { if videoMessage.video.data != nil { blob = videoMessage.video.data } name = videoMessage.blobGetWebFilename() type = "video/\(MEDIA_EXTENSION_VIDEO)" } init(audioMessage: AudioMessage) { if audioMessage.audio.data != nil { blob = audioMessage.audio.data } name = audioMessage.blobGetWebFilename() type = "audio/\(MEDIA_EXTENSION_VIDEO)" } init(fileMessage: FileMessage) { if fileMessage.data.data != nil { blob = fileMessage.data.data } name = fileMessage.blobGetWebFilename() type = fileMessage.mimeType } func objectDict() -> [String: Any] { var objectDict:[String: Any] = ["type": type, "name": name] if blob != nil { objectDict.updateValue(blob!, forKey: "blob") } return objectDict } } struct WebThumbnail { var height: Int var width: Int var preview: Data var image: Data? init(imageMessage: ImageMessage, onlyThumbnail: Bool) { var size: CGSize var useThumbnail: Bool = true if !onlyThumbnail && imageMessage.image != nil { if imageMessage.image.data != nil { useThumbnail = false } } else { if imageMessage.thumbnail == nil { useThumbnail = false } if imageMessage.thumbnail != nil { if imageMessage.thumbnail.data == nil { useThumbnail = false } } } if useThumbnail == true { size = MediaConverter.getWebThumbnailSize(forImageData: imageMessage.thumbnail.data) preview = MediaConverter.getWebPreviewData(imageMessage.thumbnail.data) } else { size = MediaConverter.getWebThumbnailSize(forImageData: imageMessage.image.data) preview = MediaConverter.getWebPreviewData(imageMessage.image.data) } height = Int(size.height) width = Int(size.width) if !onlyThumbnail { if useThumbnail == true { image = MediaConverter.getWebThumbnailData(imageMessage.thumbnail.data) } else { image = MediaConverter.getWebThumbnailData(imageMessage.image.data) } } } init(_ videoMessage: VideoMessage, onlyThumbnail: Bool) { let size = MediaConverter.getWebThumbnailSize(forImageData: videoMessage.thumbnail.data) height = Int(size.height) width = Int(size.width) if let tmpPreview = MediaConverter.getWebPreviewData(videoMessage.thumbnail.data) { preview = tmpPreview } else { height = Int(44) width = Int(44) preview = UIImage.init(named: "Thumbnail")!.pngData()! } if !onlyThumbnail { image = MediaConverter.getWebThumbnailData(videoMessage.thumbnail.data) } } init(_ fileMessage: FileMessage, onlyThumbnail: Bool) { let size = MediaConverter.getWebThumbnailSize(forImageData: fileMessage.thumbnail.data) height = Int(size.height) width = Int(size.width) if let tmpPreview = MediaConverter.getWebPreviewData(fileMessage.thumbnail.data) { preview = tmpPreview } else { height = Int(44) width = Int(44) preview = UIImage.init(named: "Thumbnail")!.pngData()! } if !onlyThumbnail { image = MediaConverter.getWebThumbnailData(fileMessage.thumbnail.data) } } func objectDict() -> [String: Any] { return ["height": height, "width": width, "preview": preview] } } struct WebVideo { var duration: Int var size: Int init(_ videoMessage: VideoMessage) { duration = videoMessage.duration.intValue size = videoMessage.videoSize.intValue } init (_ fileMessage: FileMessage) { if let videoDuration = fileMessage.duration { duration = videoDuration.intValue } else { duration = 0 } if let videoSize = fileMessage.fileSize { size = videoSize.intValue } else { size = 0 } } func objectDict() -> [String: Any] { return ["duration": duration, "size": size] } } struct WebAudio { var duration: Int init(_ audioMessage: AudioMessage) { duration = audioMessage.duration.intValue } func objectDict() -> [String: Any] { return ["duration": duration] } } struct WebLocation { var lat: Float var lon: Float var accuracy: Float var address: String? var description: String? init(_ locationMessage: LocationMessage) { lat = locationMessage.latitude.floatValue lon = locationMessage.longitude.floatValue accuracy = locationMessage.accuracy.floatValue address = nil description = locationMessage.poiName } func objectDict() -> [String: Any] { var objectDict:[String: Any] = ["lat": lat, "lon": lon, "accuracy": accuracy] if address != nil { objectDict.updateValue(address!, forKey: "address") } if description != nil { objectDict.updateValue(description!, forKey: "description") } return objectDict } } struct WebFile { var name: String var size: Int var type: String var inApp: Bool init(_ fileMessage: FileMessage) { name = fileMessage.fileName size = fileMessage.fileSize.intValue type = fileMessage.mimeType inApp = false } func objectDict() -> [String: Any] { return ["name": name, "size": size, "type": type, "inApp": inApp] } } struct WebVoip { var status: Int var duration: Int? var reason: Int? init(_ systemMessage: SystemMessage) { switch systemMessage.type.intValue { case kSystemMessageCallMissed: status = 1 break case kSystemMessageCallRejected: status = 3 break case kSystemMessageCallRejectedBusy: status = 3 break case kSystemMessageCallRejectedTimeout: status = 3 break case kSystemMessageCallEnded: if systemMessage.haveCallTime() { status = 2 } else { status = 4 } break case kSystemMessageCallRejectedDisabled: status = 3 break default: status = 2 } if let callTime = systemMessage.callTime() { duration = Int(DateFormatter.totalSeconds(callTime)) } switch systemMessage.type.intValue { case kSystemMessageCallRejected: reason = 3 break case kSystemMessageCallRejectedBusy: reason = 1 break case kSystemMessageCallRejectedTimeout: reason = 2 break case kSystemMessageCallRejectedDisabled: reason = 4 break default: break } } func objectDict() -> [String: Any] { var objectDict:[String: Any] = ["status": status] if duration != nil { objectDict.updateValue(duration!, forKey: "duration") } if reason != nil { objectDict.updateValue(reason!, forKey: "reason") } return objectDict } } extension Data { var integer: UInt64 { return withUnsafeBytes { $0.pointee } } } extension Numeric { var makeData: Data { var source = self // This will return 1 byte for 8-bit, 2 bytes for 16-bit, 4 bytes for 32-bit and 8 bytes for 64-bit binary integers. For floating point types it will return 4 bytes for single-precision, 8 bytes for double-precision and 16 bytes for extended precision. return Data(bytes: &source, count: MemoryLayout.size) } }