diff --git a/SessionMessagingKit/Jobs/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/MessageReceiveJob.swift index 13d8ee713..1049b8a11 100644 --- a/SessionMessagingKit/Jobs/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/MessageReceiveJob.swift @@ -39,8 +39,8 @@ public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSC Configuration.shared.storage.withAsync({ transaction in // Intentionally capture self Threading.workQueue.async { do { - let message = try MessageReceiver.parse(self.data, messageServerID: self.messageServerID, using: transaction) - try MessageReceiver.handle(message, using: transaction) + let (message, proto) = try MessageReceiver.parse(self.data, messageServerID: self.messageServerID, using: transaction) + try MessageReceiver.handle(message, associatedWithProto: proto, using: transaction) self.handleSuccess() } catch { SNLog("Couldn't parse message due to error: \(error).") diff --git a/SessionMessagingKit/Messages/Visible Message/VisibleMessage+Attachment.swift b/SessionMessagingKit/Messages/Visible Message/VisibleMessage+Attachment.swift new file mode 100644 index 000000000..96d180e68 --- /dev/null +++ b/SessionMessagingKit/Messages/Visible Message/VisibleMessage+Attachment.swift @@ -0,0 +1,60 @@ +import CoreGraphics +import SessionUtilitiesKit + +public extension VisibleMessage { + + @objc(SNAttachment) + class Attachment : NSObject, NSCoding { + public var fileName: String? + public var contentType: String? + public var key: Data? + public var digest: Data? + public var kind: Kind? + public var caption: String? + public var size: CGSize? + public var sizeInBytes: UInt? + public var url: String? + + public var isValid: Bool { + fileName != nil && contentType != nil && key != nil && digest != nil && kind != nil && size != nil && sizeInBytes != nil && url != nil + } + + public enum Kind : String { + case voiceMessage, generic + } + + public override init() { super.init() } + + public required init?(coder: NSCoder) { + if let fileName = coder.decodeObject(forKey: "fileName") as! String? { self.fileName = fileName } + if let contentType = coder.decodeObject(forKey: "contentType") as! String? { self.contentType = contentType } + if let key = coder.decodeObject(forKey: "key") as! Data? { self.key = key } + if let digest = coder.decodeObject(forKey: "digest") as! Data? { self.digest = digest } + if let rawKind = coder.decodeObject(forKey: "kind") as! String? { self.kind = Kind(rawValue: rawKind) } + if let caption = coder.decodeObject(forKey: "caption") as! String? { self.caption = caption } + if let size = coder.decodeObject(forKey: "size") as! CGSize? { self.size = size } + if let sizeInBytes = coder.decodeObject(forKey: "sizeInBytes") as! UInt? { self.sizeInBytes = sizeInBytes } + if let url = coder.decodeObject(forKey: "url") as! String? { self.url = url } + } + + public func encode(with coder: NSCoder) { + coder.encode(fileName, forKey: "fileName") + coder.encode(contentType, forKey: "contentType") + coder.encode(key, forKey: "key") + coder.encode(digest, forKey: "digest") + coder.encode(kind?.rawValue, forKey: "kind") + coder.encode(caption, forKey: "caption") + coder.encode(size, forKey: "size") + coder.encode(sizeInBytes, forKey: "sizeInBytes") + coder.encode(url, forKey: "url") + } + + public static func fromProto(_ proto: SNProtoAttachmentPointer) -> Attachment? { + preconditionFailure("Use MessageReceiverDelegate.parseAttachments(from:) instead.") + } + + public func toProto() -> SNProtoDataMessageQuote? { + fatalError("Not implemented.") + } + } +} diff --git a/SessionMessagingKit/Messages/Visible Message/VisibleMessage.swift b/SessionMessagingKit/Messages/Visible Message/VisibleMessage.swift index 05f6839d4..e0339d25f 100644 --- a/SessionMessagingKit/Messages/Visible Message/VisibleMessage.swift +++ b/SessionMessagingKit/Messages/Visible Message/VisibleMessage.swift @@ -38,7 +38,7 @@ public final class VisibleMessage : Message { guard let dataMessage = proto.dataMessage else { return nil } let result = VisibleMessage() result.text = dataMessage.body - result.attachmentIDs = [] // TODO: Attachments + // Attachments are handled in MessageReceiver if let quoteProto = dataMessage.quote, let quote = Quote.fromProto(quoteProto) { result.quote = quote } if let linkPreviewProto = dataMessage.preview.first, let linkPreview = LinkPreview.fromProto(linkPreviewProto) { result.linkPreview = linkPreview } // TODO: Contact diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index 2f9b3b4cc..b8917dd72 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -47,7 +47,7 @@ internal enum MessageReceiver { } } - internal static func parse(_ data: Data, messageServerID: UInt64?, using transaction: Any) throws -> Message { + internal static func parse(_ data: Data, messageServerID: UInt64?, using transaction: Any) throws -> (Message, SNProtoContent) { // Parse the envelope let envelope = try SNProtoEnvelope.parseData(data) // Decrypt the contents @@ -90,13 +90,13 @@ internal enum MessageReceiver { message.groupPublicKey = groupPublicKey message.openGroupServerMessageID = messageServerID guard message.isValid else { throw Error.invalidMessage } - return message + return (message, proto) } else { throw Error.unknownMessage } } - internal static func handle(_ message: Message, using transaction: Any) throws { + internal static func handle(_ message: Message, associatedWithProto proto: SNProtoContent, using transaction: Any) throws { switch message { case let message as ReadReceipt: handleReadReceipt(message, using: transaction) case let message as SessionRequest: handleSessionRequest(message, using: transaction) @@ -104,7 +104,7 @@ internal enum MessageReceiver { case let message as TypingIndicator: handleTypingIndicator(message, using: transaction) case let message as ClosedGroupUpdate: handleClosedGroupUpdate(message, using: transaction) case let message as ExpirationTimerUpdate: handleExpirationTimerUpdate(message, using: transaction) - case let message as VisibleMessage: try handleVisibleMessage(message, using: transaction) + case let message as VisibleMessage: try handleVisibleMessage(message, associatedWithProto: proto, using: transaction) default: fatalError() } } @@ -148,14 +148,18 @@ internal enum MessageReceiver { } } - private static func handleVisibleMessage(_ message: VisibleMessage, using transaction: Any) throws { + private static func handleVisibleMessage(_ message: VisibleMessage, associatedWithProto proto: SNProtoContent, using transaction: Any) throws { let delegate = Configuration.shared.messageReceiverDelegate + let storage = Configuration.shared.storage + // Handle attachments + let attachments = delegate.parseAttachments(from: proto.dataMessage!.attachments) + message.attachmentIDs = storage.save(attachments, using: transaction) // Update profile if needed if let profile = message.profile { delegate.updateProfile(for: message.sender!, from: profile, using: transaction) } // Persist the message - guard let (threadID, tsIncomingMessage) = Configuration.shared.storage.persist(message, groupPublicKey: message.groupPublicKey, using: transaction) else { throw Error.noThread } + guard let (threadID, tsIncomingMessage) = storage.persist(message, groupPublicKey: message.groupPublicKey, using: transaction) else { throw Error.noThread } message.threadID = threadID // Cancel any typing indicators delegate.cancelTypingIndicatorsIfNeeded(for: message.sender!) diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiverDelegate.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiverDelegate.swift index cba87224d..34e289e41 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiverDelegate.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiverDelegate.swift @@ -14,4 +14,5 @@ public protocol MessageReceiverDelegate { func handleGroupUpdate(_ message: ClosedGroupUpdate, using transaction: Any) func handleSenderKeyRequest(_ message: ClosedGroupUpdate, using transaction: Any) func handleSenderKey(_ message: ClosedGroupUpdate, using transaction: Any) + func parseAttachments(from protos: [SNProtoAttachmentPointer]) -> [VisibleMessage.Attachment] } diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 42ce0351f..71b0d3fab 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -42,7 +42,6 @@ public protocol SessionMessagingKitStorageProtocol { func setOpenGroupPublicKey(for server: String, to newValue: String, using transaction: Any) // MARK: - Last Message Server ID - func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64? func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any) @@ -64,4 +63,6 @@ public protocol SessionMessagingKitStorageProtocol { /// Returns the ID of the thread the message was stored under along with the `TSIncomingMessage` that was constructed. func persist(_ message: VisibleMessage, groupPublicKey: String?, using transaction: Any) -> (String, Any)? + /// Returns the IDs of the saved attachments. + func save(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] } diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 59f0a69de..ab725d155 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -585,6 +585,8 @@ C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */ = {isa = PBXBuildFile; fileRef = C35E8AAD2485E51D00ACB629 /* IP2Country.swift */; }; C3645350252449260045C478 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364534F252449260045C478 /* VoiceMessageView.swift */; }; C364535C252467900045C478 /* AudioUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C364535B252467900045C478 /* AudioUtilities.swift */; }; + C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; }; + C379DCFE25673DBC0002D4EB /* Attachment+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCFD25673DBC0002D4EB /* Attachment+Conversion.swift */; }; C37F5385255B94F6002AEA92 /* SelectRecipientViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF34E255B6DC8007E1867 /* SelectRecipientViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; C37F5396255B95BD002AEA92 /* OWSAnyTouchGestureRecognizer.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */; settings = {ATTRIBUTES = (Public, ); }; }; C37F53A7255B96E0002AEA92 /* OWSAudioPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1679,6 +1681,8 @@ C35E8AAD2485E51D00ACB629 /* IP2Country.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IP2Country.swift; sourceTree = ""; }; C364534F252449260045C478 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C364535B252467900045C478 /* AudioUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioUtilities.swift; sourceTree = ""; }; + C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+Attachment.swift"; sourceTree = ""; }; + C379DCFD25673DBC0002D4EB /* Attachment+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Attachment+Conversion.swift"; sourceTree = ""; }; C37F53E8255BA9BB002AEA92 /* Environment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Environment.h; sourceTree = ""; }; C37F5402255BA9ED002AEA92 /* Environment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Environment.m; sourceTree = ""; }; C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = ""; }; @@ -2631,6 +2635,7 @@ isa = PBXGroup; children = ( C3C2A74C2553A39700C340D1 /* VisibleMessage.swift */, + C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */, C3C2A7552553A3AB00C340D1 /* VisibleMessage+Quote.swift */, C3C2A75E2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift */, C3C2A7672553A3D900C340D1 /* VisibleMessage+Contact.swift */, @@ -3279,6 +3284,7 @@ C38BBA0B255E31EC0041B9A3 /* Attachments */ = { isa = PBXGroup; children = ( + C379DCFD25673DBC0002D4EB /* Attachment+Conversion.swift */, C38EF224255B6D5D007E1867 /* SignalAttachment.swift */, C33FDC15255A581E00E217F9 /* TSAttachment.h */, C33FDAC2255A580200E217F9 /* TSAttachment.m */, @@ -4957,6 +4963,7 @@ C33FDD3D255A582000E217F9 /* TSQuotedMessage.m in Sources */, C38EF273255B6D7A007E1867 /* OWSDatabaseMigrationRunner.m in Sources */, C33FDD12255A582000E217F9 /* OWSPrimaryStorage+Loki.m in Sources */, + C379DCFE25673DBC0002D4EB /* Attachment+Conversion.swift in Sources */, C38EF31A255B6DBF007E1867 /* OWSAnyTouchGestureRecognizer.m in Sources */, C33FDCFB255A582000E217F9 /* MIMETypeUtil.m in Sources */, C38EF36C255B6DCC007E1867 /* SharingThreadPickerViewController.m in Sources */, @@ -5263,6 +5270,7 @@ C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */, C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */, C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */, + C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */, C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */, C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */, diff --git a/SignalUtilitiesKit/Database/Storage/Storage+Messaging.swift b/SignalUtilitiesKit/Database/Storage/Storage+Messaging.swift index 50a4fe4bb..ca05b565c 100644 --- a/SignalUtilitiesKit/Database/Storage/Storage+Messaging.swift +++ b/SignalUtilitiesKit/Database/Storage/Storage+Messaging.swift @@ -32,4 +32,12 @@ extension Storage { message.save(with: transaction) return (thread.uniqueId!, message) } + + public func save(_ attachments: [VisibleMessage.Attachment], using transaction: Any) -> [String] { + return attachments.map { attachment in + let tsAttachment = TSAttachmentPointer.from(attachment) + tsAttachment.save(with: transaction as! YapDatabaseReadWriteTransaction) + return tsAttachment.uniqueId! + } + } } diff --git a/SignalUtilitiesKit/Messaging/Attachments/Attachment+Conversion.swift b/SignalUtilitiesKit/Messaging/Attachments/Attachment+Conversion.swift new file mode 100644 index 000000000..c4b595f90 --- /dev/null +++ b/SignalUtilitiesKit/Messaging/Attachments/Attachment+Conversion.swift @@ -0,0 +1,22 @@ + +extension TSAttachmentPointer { + + public static func from(_ attachment: VisibleMessage.Attachment) -> TSAttachmentPointer { + let kind: TSAttachmentType + switch attachment.kind! { + case .generic: kind = .default + case .voiceMessage: kind = .voiceMessage + } + return TSAttachmentPointer( + serverId: 0, + key: attachment.key, + digest: attachment.digest, + byteCount: UInt32(attachment.sizeInBytes!), + contentType: attachment.contentType!, + sourceFilename: attachment.fileName, + caption: attachment.caption, + albumMessageId: nil, + attachmentType: kind, + mediaSize: attachment.size!) + } +} diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageReceiverDelegate.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageReceiverDelegate.swift index 84eb02a3f..a8efdb92a 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageReceiverDelegate.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageReceiverDelegate.swift @@ -312,4 +312,40 @@ public final class MessageReceiverDelegate : SessionMessagingKit.MessageReceiver let ratchet = ClosedGroupRatchet(chainKey: senderKey.chainKey.toHexString(), keyIndex: UInt(senderKey.keyIndex), messageKeys: []) Storage.shared.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: message.sender!, ratchet: ratchet, using: transaction) } + + + + // MARK: - Attachments + + public func parseAttachments(from protos: [SNProtoAttachmentPointer]) -> [VisibleMessage.Attachment] { + return protos.compactMap { proto in + let result = VisibleMessage.Attachment() + result.fileName = proto.fileName + func inferContentType() -> String { + guard let fileName = result.fileName, let fileExtension = URL(string: fileName)?.pathExtension else { return OWSMimeTypeApplicationOctetStream } + return MIMETypeUtil.mimeType(forFileExtension: fileExtension) ?? OWSMimeTypeApplicationOctetStream + } + result.contentType = proto.contentType ?? inferContentType() + result.key = proto.key + result.digest = proto.digest + let kind: VisibleMessage.Attachment.Kind + if proto.hasFlags && (proto.flags & UInt32(SNProtoAttachmentPointer.SNProtoAttachmentPointerFlags.voiceMessage.rawValue)) > 0 { + kind = .voiceMessage + } else { + kind = .generic + } + result.kind = kind + result.caption = proto.hasCaption ? proto.caption : nil + let size: CGSize + if proto.hasWidth && proto.width > 0 && proto.hasHeight && proto.height > 0 { + size = CGSize(width: Int(proto.width), height: Int(proto.height)) + } else { + size = CGSize.zero + } + result.size = size + result.sizeInBytes = proto.size > 0 ? UInt(proto.size) : nil + result.url = proto.url + return result.isValid ? result : nil + } + } }