From 362e2e9c03c95c7c9f53f757ed87174ce8c78592 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Mon, 9 Nov 2020 11:53:07 +1100 Subject: [PATCH] Update SessionMessagingKit for open groups --- Podfile | 1 + Podfile.lock | 3 +- Pods | 2 +- SessionMessagingKit/Configuration.swift | 1 + .../File Server/FileServerAPI.swift | 75 +++++ .../Open Groups/OpenGroup.swift | 6 +- .../Open Groups/OpenGroupAPI.swift | 261 +++++++----------- .../Open Groups/OpenGroupAPIDelegate.swift | 5 + .../Open Groups/OpenGroupInfo.swift | 2 +- .../Open Groups/OpenGroupMessage.swift | 33 ++- SessionMessagingKit/Storage.swift | 17 ++ .../Utilities/AttachmentStream.swift | 11 + .../Utilities}/DotNetAPI.swift | 171 ++++++------ .../Shared Sender Keys/SharedSenderKeys.swift | 10 +- .../Signal/FallbackSessionCipher.swift | 4 +- .../Signal/LokiSessionCipher.swift | 2 +- .../Signal/SessionRestorationProtocol.swift | 2 +- .../Signal/SessionRestorationStatus.swift | 2 +- .../AnyPromise+Conversion.swift | 2 +- .../AnyPromise+Retaining.swift | 17 ++ SessionUtilitiesKit/Mnemonic.swift | 2 +- Signal.xcodeproj/project.pbxproj | 56 ++++ 22 files changed, 396 insertions(+), 289 deletions(-) create mode 100644 SessionMessagingKit/File Server/FileServerAPI.swift rename SignalServiceKit/src/Loki/API/Open Groups/PublicChat.swift => SessionMessagingKit/Open Groups/OpenGroup.swift (90%) rename SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift => SessionMessagingKit/Open Groups/OpenGroupAPI.swift (64%) create mode 100644 SessionMessagingKit/Open Groups/OpenGroupAPIDelegate.swift rename SignalServiceKit/src/Loki/API/Open Groups/PublicChatInfo.swift => SessionMessagingKit/Open Groups/OpenGroupInfo.swift (78%) rename SignalServiceKit/src/Loki/API/Open Groups/PublicChatMessage.swift => SessionMessagingKit/Open Groups/OpenGroupMessage.swift (82%) create mode 100644 SessionMessagingKit/Utilities/AttachmentStream.swift rename {SignalServiceKit/src/Loki/API => SessionMessagingKit/Utilities}/DotNetAPI.swift (55%) rename {SignalServiceKit/src/Loki/Utilities => SessionUtilitiesKit}/AnyPromise+Conversion.swift (68%) create mode 100644 SessionUtilitiesKit/AnyPromise+Retaining.swift diff --git a/Podfile b/Podfile index d4ec63eaf..67e3e8a37 100644 --- a/Podfile +++ b/Podfile @@ -99,6 +99,7 @@ target 'SignalMessaging' do end target 'SessionMessagingKit' do + pod 'AFNetworking', inhibit_warnings: true pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 32abf08b8..096a79587 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -197,6 +197,7 @@ PODS: - ZXingObjC/All (3.6.5) DEPENDENCIES: + - AFNetworking - AFNetworking (~> 3.2.1) - CocoaLumberjack - CryptoSwift @@ -332,6 +333,6 @@ SPEC CHECKSUMS: YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 278b25019daa575575de0bf9baf371f7cdcd4fc4 +PODFILE CHECKSUM: 8fc5917e97576b902a46b328af80664381ede889 COCOAPODS: 1.10.0.rc.1 diff --git a/Pods b/Pods index 38df69dfa..2bd2badeb 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 38df69dfa06ce734db625bdf39534d654528a916 +Subproject commit 2bd2badebb8fd11a6f7312b2c490cf9a70a09874 diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 81c52e88e..4ad275f03 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -4,6 +4,7 @@ public struct Configuration { public let storage: SessionMessagingKitStorageProtocol public let sessionRestorationImplementation: SessionRestorationProtocol public let certificateValidator: SMKCertificateValidator + public let openGroupAPIDelegate: OpenGroupAPIDelegate public let pnServerURL: String public let pnServerPublicKey: String diff --git a/SessionMessagingKit/File Server/FileServerAPI.swift b/SessionMessagingKit/File Server/FileServerAPI.swift new file mode 100644 index 000000000..957e8ff80 --- /dev/null +++ b/SessionMessagingKit/File Server/FileServerAPI.swift @@ -0,0 +1,75 @@ +import AFNetworking +import PromiseKit +import SessionSnodeKit +import SessionUtilitiesKit + +@objc(SNFileServerAPI) +public final class FileServerAPI : DotNetAPI { + + // MARK: Settings + private static let attachmentType = "net.app.core.oembed" + private static let deviceLinkType = "network.loki.messenger.devicemapping" + + internal static let publicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C" + + public static let maxFileSize = 10_000_000 // 10 MB + /// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes + /// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP + /// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also + /// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when + /// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only + /// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. + public static let fileSizeORMultiplier: Double = 3 + + @objc public static let server = "https://file.getsession.org" + @objc public static let fileStorageBucketURL = "https://file-static.lokinet.org" + + // MARK: Profile Pictures + @objc(uploadProfilePicture:) + public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise { + return AnyPromise.from(uploadProfilePicture(profilePicture)) + } + + public static func uploadProfilePicture(_ profilePicture: Data) -> Promise { + guard Double(profilePicture.count) < Double(maxFileSize) / fileSizeORMultiplier else { return Promise(error: Error.maxFileSizeExceeded) } + let url = "\(server)/files" + let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] + var error: NSError? + let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in + formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary") + }, error: &error) + // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token + request.addValue("Bearer loki", forHTTPHeaderField: "Authorization") + if let error = error { + SNLog("Couldn't upload profile picture due to error: \(error).") + return Promise(error: error) + } + return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { + SNLog("Couldn't parse profile picture from: \(json).") + throw Error.parsingFailed + } + Configuration.shared.storage.setLastProfilePictureUploadDate(Date()) + return downloadURL + } + } + + // MARK: Open Group Server Public Key + public static func getPublicKey(for openGroupServer: String) -> Promise { + let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(URL(string: openGroupServer)!.host!)")! + let request = TSRequest(url: url) + let token = "loki" // Tokenless request; use a dummy token + request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] + return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in + guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8), + let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON } + guard let base64EncodedPublicKey = body["data"] as? String else { + SNLog("Couldn't parse open group public key from: \(body).") + throw Error.parsingFailed + } + let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)! + let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString() + return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded() + } + } +} diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChat.swift b/SessionMessagingKit/Open Groups/OpenGroup.swift similarity index 90% rename from SignalServiceKit/src/Loki/API/Open Groups/PublicChat.swift rename to SessionMessagingKit/Open Groups/OpenGroup.swift index 47905ce5d..2c41221e6 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChat.swift +++ b/SessionMessagingKit/Open Groups/OpenGroup.swift @@ -1,6 +1,6 @@ -@objc(LKPublicChat) -public final class PublicChat : NSObject, NSCoding { +@objc(SNOpenGroup) +public final class OpenGroup : NSObject, NSCoding { @objc public let id: String @objc public let idAsData: Data @objc public let channel: UInt64 @@ -39,5 +39,5 @@ public final class PublicChat : NSObject, NSCoding { coder.encode(isDeletable, forKey: "isDeletable") } - override public var description: String { return "\(displayName) (\(server))" } + override public var description: String { "\(displayName) (\(server))" } } diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift similarity index 64% rename from SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift rename to SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 1661414bc..7b582952f 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -1,11 +1,12 @@ +import AFNetworking import PromiseKit +import SessionSnodeKit +import SessionUtilitiesKit -@objc(LKPublicChatAPI) -public final class PublicChatAPI : DotNetAPI { +@objc(SNOpenGroupAPI) +public final class OpenGroupAPI : DotNetAPI { private static var moderators: [String:[UInt64:Set]] = [:] // Server URL to (channel ID to set of moderator IDs) - @objc public static let defaultChats: [PublicChat] = [] // Currently unused - public static var displayNameUpdatees: [String:Set] = [:] // MARK: Settings @@ -16,71 +17,19 @@ public final class PublicChatAPI : DotNetAPI { public static let profilePictureType = "network.loki.messenger.avatar" - @objc public static let publicChatMessageType = "network.loki.messenger.publicChat" + @objc public static let openGroupMessageType = "network.loki.messenger.openGroup" - // MARK: Convenience - private static var userDisplayName: String { - let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey() - return SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey) ?? "Anonymous" - } - - // MARK: Database - override internal class var authTokenCollection: String { "LokiGroupChatAuthTokenCollection" } - - @objc public static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" - @objc public static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" - - private static func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? { - var result: UInt? = nil - Storage.read { transaction in - result = transaction.object(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection) as! UInt? - } - return result - } - - private static func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: YapDatabaseReadWriteTransaction) { - transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection) - } - - private static func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) { - transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastMessageServerIDCollection) - } - - private static func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt? { - var result: UInt? = nil - Storage.read { transaction in - result = transaction.object(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) as! UInt? - } - return result - } - - private static func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: YapDatabaseReadWriteTransaction) { - transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) - } - - private static func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: YapDatabaseReadWriteTransaction) { - transaction.removeObject(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) - } - - public static func clearCaches(for channel: UInt64, on server: String) { - Storage.writeSync { transaction in - removeLastMessageServerID(for: channel, on: server, using: transaction) - removeLastDeletionServerID(for: channel, on: server, using: transaction) - Storage.removeOpenGroupPublicKey(for: server, using: transaction) - } - } - // MARK: Open Group Public Key Validation public static func getOpenGroupServerPublicKey(for server: String) -> Promise { - if let publicKey = Storage.getOpenGroupPublicKey(for: server) { + if let publicKey = Configuration.shared.storage.getOpenGroupPublicKey(for: server) { return Promise.value(publicKey) } else { return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise in let url = URL(string: server)! let request = TSRequest(url: url) return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in - Storage.writeSync { transaction in - Storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) + Configuration.shared.storage.with { transaction in + Configuration.shared.storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction) } return publicKey } @@ -94,61 +43,62 @@ public final class PublicChatAPI : DotNetAPI { return AnyPromise.from(getMessages(for: group, on: server)) } - public static func getMessages(for channel: UInt64, on server: String) -> Promise<[PublicChatMessage]> { + public static func getMessages(for channel: UInt64, on server: String) -> Promise<[OpenGroupMessage]> { + let storage = Configuration.shared.storage var queryParameters = "include_annotations=1" - if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) { + if let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server) { queryParameters += "&since_id=\(lastMessageServerID)" } else { queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0" } return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[PublicChatMessage]> in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[OpenGroupMessage]> in let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")! let request = TSRequest(url: url) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in guard let rawMessages = json["data"] as? [JSON] else { - print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse messages for open group channel with ID: \(channel) on server: \(server) from: \(json).") + throw Error.parsingFailed } - return rawMessages.flatMap { message in + return rawMessages.compactMap { message in let isDeleted = (message["is_deleted"] as? Int == 1) guard !isDeleted else { return nil } let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == publicChatMessageType }), let value = annotation["value"] as? JSON, + guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == openGroupMessageType }), let value = annotation["value"] as? JSON, let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64, let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let timestamp = value["timestamp"] as? UInt64, let dateAsString = message["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else { - print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(message).") + SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(message).") return nil } let serverTimestamp = UInt64(date.timeIntervalSince1970) * 1000 - var profilePicture: PublicChatMessage.ProfilePicture? = nil + var profilePicture: OpenGroupMessage.ProfilePicture? = nil let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "") if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }), let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String { - profilePicture = PublicChatMessage.ProfilePicture(profileKey: profileKey, url: url) + profilePicture = OpenGroupMessage.ProfilePicture(profileKey: profileKey, url: url) } - let lastMessageServerID = getLastMessageServerID(for: channel, on: server) + let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server) if serverID > (lastMessageServerID ?? 0) { - Storage.writeSync { transaction in - setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction) + storage.with { transaction in + storage.setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction) } } - let quote: PublicChatMessage.Quote? + let quote: OpenGroupMessage.Quote? if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String, let quotedMessageBody = quoteAsJSON["text"] as? String { let quotedMessageServerID = message["reply_to"] as? UInt64 - quote = PublicChatMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody, + quote = OpenGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID) } else { quote = nil } - let signature = PublicChatMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion) + let signature = OpenGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion) let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType } - let attachments: [PublicChatMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in - guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = PublicChatMessage.Attachment.Kind(rawValue: kindAsString), + let attachments: [OpenGroupMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in + guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = OpenGroupMessage.Attachment.Kind(rawValue: kindAsString), let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil } let fileName = value["fileName"] as? String ?? UUID().description let width = value["width"] as? UInt ?? 0 @@ -159,25 +109,22 @@ public final class PublicChatAPI : DotNetAPI { let linkPreviewTitle = value["linkPreviewTitle"] as? String if kind == .linkPreview { guard linkPreviewURL != nil && linkPreviewTitle != nil else { - print("[Loki] Ignoring public chat message with invalid link preview.") + SNLog("Ignoring open group message with invalid link preview.") return nil } } - return PublicChatMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, + return OpenGroupMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle) } - let result = PublicChatMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture, - body: body, type: publicChatMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp) + let result = OpenGroupMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture, + body: body, type: openGroupMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp) guard result.hasValidSignature() else { - print("[Loki] Ignoring public chat message with invalid signature.") + SNLog("Ignoring open group message with invalid signature.") return nil } - var existingMessageID: String? = nil - Storage.read { transaction in - existingMessageID = OWSPrimaryStorage.shared().getIDForMessage(withServerID: UInt(result.serverID!), in: transaction) - } + let existingMessageID = storage.getIDForMessage(withServerID: UInt(result.serverID!)) guard existingMessageID == nil else { - print("[Loki] Ignoring duplicate public chat message.") + SNLog("Ignoring duplicate open group message.") return nil } return result @@ -189,18 +136,21 @@ public final class PublicChatAPI : DotNetAPI { // MARK: Sending @objc(sendMessage:toGroup:onServer:) - public static func objc_sendMessage(_ message: PublicChatMessage, to group: UInt64, on server: String) -> AnyPromise { + public static func objc_sendMessage(_ message: OpenGroupMessage, to group: UInt64, on server: String) -> AnyPromise { return AnyPromise.from(sendMessage(message, to: group, on: server)) } - public static func sendMessage(_ message: PublicChatMessage, to channel: UInt64, on server: String) -> Promise { - print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).") - let (promise, seal) = Promise.pending() - DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in - guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) } + public static func sendMessage(_ message: OpenGroupMessage, to channel: UInt64, on server: String) -> Promise { + SNLog("Sending message to open group channel with ID: \(channel) on server: \(server).") + let storage = Configuration.shared.storage + guard let userKeyPair = storage.getUserKeyPair() else { return Promise(error: Error.generic) } + guard let userDisplayName = storage.getUserDisplayName() else { return Promise(error: Error.generic) } + let (promise, seal) = Promise.pending() + DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey()] in + guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(Error.signingFailed) } attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)/messages")! let parameters = signedMessage.toJSON() let request = TSRequest(url: url, method: "POST", parameters: parameters) @@ -212,11 +162,11 @@ public final class PublicChatAPI : DotNetAPI { dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" guard let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String, let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else { - print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(json).") + throw Error.parsingFailed } let timestamp = UInt64(date.timeIntervalSince1970) * 1000 - return PublicChatMessage(serverID: serverID, senderPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp) + return OpenGroupMessage(serverID: serverID, senderPublicKey: userKeyPair.publicKey()!.toHexString(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: openGroupMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp) } } }.handlingInvalidAuthTokenIfNeeded(for: server) @@ -231,9 +181,10 @@ public final class PublicChatAPI : DotNetAPI { // MARK: Deletion public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> { - print("[Loki] Getting deleted messages for public chat channel with ID: \(channel) on server: \(server).") + SNLog("Getting deleted messages for open group channel with ID: \(channel) on server: \(server).") + let storage = Configuration.shared.storage let queryParameters: String - if let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) { + if let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server) { queryParameters = "since_id=\(lastDeletionServerID)" } else { queryParameters = "count=\(fallbackBatchCount)" @@ -245,18 +196,18 @@ public final class PublicChatAPI : DotNetAPI { request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in guard let body = json["body"] as? JSON, let deletions = body["data"] as? [JSON] else { - print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse deleted messages for open group channel with ID: \(channel) on server: \(server) from: \(json).") + throw Error.parsingFailed } - return deletions.flatMap { deletion in + return deletions.compactMap { deletion in guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else { - print("[Loki] Couldn't parse deleted message for public chat channel with ID: \(channel) on server: \(server) from: \(deletion).") + SNLog("Couldn't parse deleted message for open group channel with ID: \(channel) on server: \(server) from: \(deletion).") return nil } - let lastDeletionServerID = getLastDeletionServerID(for: channel, on: server) + let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server) if serverID > (lastDeletionServerID ?? 0) { - Storage.writeSync { transaction in - setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction) + storage.with { transaction in + storage.setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction) } } return messageServerID @@ -273,7 +224,7 @@ public final class PublicChatAPI : DotNetAPI { public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise { let isModerationRequest = !isSentByUser - print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).") + SNLog("Deleting message with ID: \(messageID) for open group channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).") let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)" return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in @@ -282,7 +233,7 @@ public final class PublicChatAPI : DotNetAPI { let request = TSRequest(url: url, method: "DELETE", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in - print("[Loki] Deleted message with ID: \(messageID) on server: \(server).") + SNLog("Deleted message with ID: \(messageID) on server: \(server).") } } }.handlingInvalidAuthTokenIfNeeded(for: server) @@ -291,10 +242,10 @@ public final class PublicChatAPI : DotNetAPI { // MARK: Display Name & Profile Picture public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise { - let publicChatID = "\(server).\(channel)" - guard let publicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) } - displayNameUpdatees[publicChatID] = [] - print("[Loki] Getting display names for: \(publicKeys).") + let openGroupID = "\(server).\(channel)" + guard let publicKeys = displayNameUpdatees[openGroupID] else { return Promise.value(()) } + displayNameUpdatees[openGroupID] = [] + SNLog("Getting display names for: \(publicKeys).") return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1" @@ -302,16 +253,17 @@ public final class PublicChatAPI : DotNetAPI { let request = TSRequest(url: url) return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in guard let data = json["data"] as? [JSON] else { - print("[Loki] Couldn't parse display names for users: \(publicKeys) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse display names for users: \(publicKeys) from: \(json).") + throw Error.parsingFailed } - Storage.writeSync { transaction in + let storage = Configuration.shared.storage + storage.with { transaction in data.forEach { data in guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return } let endIndex = hexEncodedPublicKey.endIndex let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8) let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex.. Promise { - print("[Loki] Updating display name on server: \(server).") + SNLog("Updating display name on server: \(server).") let parameters: JSON = [ "name" : (newDisplayName ?? "") ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in @@ -348,7 +300,7 @@ public final class PublicChatAPI : DotNetAPI { } public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise { - print("[Loki] Updating profile picture on server: \(server).") + SNLog("Updating profile picture on server: \(server).") var annotation: JSON = [ "type" : profilePictureType ] if let url = url { annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ] @@ -361,7 +313,7 @@ public final class PublicChatAPI : DotNetAPI { let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in - print("[Loki] Couldn't update profile picture due to error: \(error).") + SNLog("Couldn't update profile picture due to error: \(error).") throw error } } @@ -369,46 +321,16 @@ public final class PublicChatAPI : DotNetAPI { } } - static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: PublicChatInfo) { - let storage = OWSPrimaryStorage.shared() - let publicChatID = "\(server).\(channel)" - Storage.writeSync { transaction in - // Update user count - storage.setUserCount(info.memberCount, forPublicChatWithID: publicChatID, in: transaction) - let groupThread = TSGroupThread.getOrCreateThread(withGroupId: publicChatID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction) - // Update display name if needed - let groupModel = groupThread.groupModel - if groupModel.groupName != info.displayName { - let newGroupModel = TSGroupModel(title: info.displayName, memberIds: groupModel.groupMemberIds, image: groupModel.groupImage, groupId: groupModel.groupId, groupType: groupModel.groupType, adminIds: groupModel.groupAdminIds) - groupThread.groupModel = newGroupModel - groupThread.save(with: transaction) - } - // Download and update profile picture if needed - let oldProfilePictureURL = storage.getProfilePictureURL(forPublicChatWithID: publicChatID, in: transaction) - if oldProfilePictureURL != info.profilePictureURL || groupModel.groupImage == nil { - storage.setProfilePictureURL(info.profilePictureURL, forPublicChatWithID: publicChatID, in: transaction) - if let profilePictureURL = info.profilePictureURL { - let url = server.hasSuffix("/") ? "\(server)\(profilePictureURL)" : "\(server)/\(profilePictureURL)" - FileServerAPI.downloadAttachment(from: url).map2 { data in - let attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil) - try attachmentStream.write(data) - groupThread.updateAvatar(with: attachmentStream) - } - } - } - } - } - // MARK: Joining & Leaving @objc(getInfoForChannelWithID:onServer:) public static func objc_getInfo(for channel: UInt64, on server: String) -> AnyPromise { return AnyPromise.from(getInfo(for: channel, on: server)) } - public static func getInfo(for channel: UInt64, on server: String) -> Promise { + public static func getInfo(for channel: UInt64, on server: String) -> Promise { return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) { getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in - getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in + getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise in let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")! let request = TSRequest(url: url) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] @@ -421,16 +343,16 @@ public final class PublicChatAPI : DotNetAPI { let profilePictureURL = info["avatar"] as? String, let countInfo = data["counts"] as? JSON, let memberCount = countInfo["subscribers"] as? Int else { - print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse info for open group channel with ID: \(channel) on server: \(server) from: \(json).") + throw Error.parsingFailed } - let storage = OWSPrimaryStorage.shared() - Storage.writeSync { transaction in - storage.setUserCount(memberCount, forPublicChatWithID: "\(server).\(channel)", in: transaction) + let storage = Configuration.shared.storage + storage.with { transaction in + storage.setUserCount(to: memberCount, forOpenGroupWithID: "\(server).\(channel)", using: transaction) } - let publicChatInfo = PublicChatInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) - updateProfileIfNeeded(for: channel, on: server, from: publicChatInfo) - return publicChatInfo + let openGroupInfo = OpenGroupInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount) + Configuration.shared.openGroupAPIDelegate.updateProfileIfNeeded(for: channel, on: server, from: openGroupInfo) + return openGroupInfo } } }.handlingInvalidAuthTokenIfNeeded(for: server) @@ -445,7 +367,7 @@ public final class PublicChatAPI : DotNetAPI { let request = TSRequest(url: url, method: "POST", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in - print("[Loki] Joined channel with ID: \(channel) on server: \(server).") + SNLog("Joined channel with ID: \(channel) on server: \(server).") } } }.handlingInvalidAuthTokenIfNeeded(for: server) @@ -460,7 +382,7 @@ public final class PublicChatAPI : DotNetAPI { let request = TSRequest(url: url, method: "DELETE", parameters: [:]) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in - print("[Loki] Left channel with ID: \(channel) on server: \(server).") + SNLog("Left channel with ID: \(channel) on server: \(server).") } } }.handlingInvalidAuthTokenIfNeeded(for: server) @@ -491,8 +413,8 @@ public final class PublicChatAPI : DotNetAPI { request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in guard let moderators = json["moderators"] as? [String] else { - print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse moderators for open group channel with ID: \(channel) on server: \(server) from: \(json).") + throw Error.parsingFailed } let moderatorsAsSet = Set(moderators); if self.moderators.keys.contains(server) { @@ -515,11 +437,14 @@ public final class PublicChatAPI : DotNetAPI { // MARK: Error Handling internal extension Promise { - internal func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise { - return recover2 { error -> Promise in + func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise { + return recover(on: DispatchQueue.global(qos: .userInitiated)) { error -> Promise in if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _) = error, statusCode == 401 || statusCode == 403 { - print("[Loki] Auth token for: \(server) expired; dropping it.") - PublicChatAPI.removeAuthToken(for: server) + SNLog("Auth token for: \(server) expired; dropping it.") + let storage = Configuration.shared.storage + storage.with { transaction in + storage.removeAuthToken(for: server, using: transaction) + } } throw error } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIDelegate.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIDelegate.swift new file mode 100644 index 000000000..a8418c4f8 --- /dev/null +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIDelegate.swift @@ -0,0 +1,5 @@ + +public protocol OpenGroupAPIDelegate { + + func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) +} diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatInfo.swift b/SessionMessagingKit/Open Groups/OpenGroupInfo.swift similarity index 78% rename from SignalServiceKit/src/Loki/API/Open Groups/PublicChatInfo.swift rename to SessionMessagingKit/Open Groups/OpenGroupInfo.swift index 49f97ca31..d842e1bf9 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatInfo.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupInfo.swift @@ -1,5 +1,5 @@ -public struct PublicChatInfo { +public struct OpenGroupInfo { public let displayName: String public let profilePictureURL: String? public let memberCount: Int diff --git a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatMessage.swift b/SessionMessagingKit/Open Groups/OpenGroupMessage.swift similarity index 82% rename from SignalServiceKit/src/Loki/API/Open Groups/PublicChatMessage.swift rename to SessionMessagingKit/Open Groups/OpenGroupMessage.swift index 9c15c618f..29034606d 100644 --- a/SignalServiceKit/src/Loki/API/Open Groups/PublicChatMessage.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupMessage.swift @@ -1,7 +1,9 @@ import PromiseKit +import Curve25519Kit +import SessionUtilitiesKit -@objc(LKPublicChatMessage) -public final class PublicChatMessage : NSObject { +@objc(SNOpenGroupMessage) +public final class OpenGroupMessage : NSObject { public let serverID: UInt64? public let senderPublicKey: String public let displayName: String @@ -74,7 +76,8 @@ public final class PublicChatMessage : NSObject { } // MARK: Initialization - public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String, type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) { + public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String, + type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) { self.serverID = serverID self.senderPublicKey = senderPublicKey self.displayName = displayName @@ -89,7 +92,9 @@ public final class PublicChatMessage : NSObject { super.init() } - @objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) { + @objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64, + quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String?, quotedMessageServerID: UInt64, + signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) { let quote: Quote? if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteePublicKey, let quotedMessageBody = quotedMessageBody { let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil @@ -107,25 +112,25 @@ public final class PublicChatMessage : NSObject { } // MARK: Crypto - internal func sign(with privateKey: Data) -> PublicChatMessage? { + internal func sign(with privateKey: Data) -> OpenGroupMessage? { guard let data = getValidationData(for: signatureVersion) else { - print("[Loki] Failed to sign public chat message.") + SNLog("Failed to sign open group message.") return nil } - let userKeyPair = OWSIdentityManager.shared().identityKeyPair()! - guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else { - print("[Loki] Failed to sign public chat message.") + let userKeyPair = Configuration.shared.storage.getUserKeyPair() + guard let signatureData = Ed25519.sign(data, with: userKeyPair) else { + SNLog("Failed to sign open group message.") return nil } let signature = Signature(data: signatureData, version: signatureVersion) - return PublicChatMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp) + return OpenGroupMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp) } internal func hasValidSignature() -> Bool { guard let signature = signature else { return false } guard let data = getValidationData(for: signature.version) else { return false } let publicKey = Data(hex: self.senderPublicKey.removing05PrefixIfNeeded()) - return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false + return Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data) } // MARK: JSON @@ -143,7 +148,6 @@ public final class PublicChatMessage : NSObject { } let annotation: JSON = [ "type" : type, "value" : value ] let attachmentAnnotations: [JSON] = attachments.map { attachment in - let type: String var attachmentValue: JSON = [ // Fields required by the .NET API "version" : 1, "type" : attachment.dotNETType, @@ -151,7 +155,7 @@ public final class PublicChatMessage : NSObject { "lokiType" : attachment.kind.rawValue, "server" : attachment.server, "id" : attachment.serverID, "contentType" : attachment.contentType, "size" : attachment.size, "fileName" : attachment.fileName, "width" : attachment.width, "height" : attachment.height, "url" : attachment.url ] if let caption = attachment.caption { - attachmentValue["caption"] = attachment.caption + attachmentValue["caption"] = caption } if let linkPreviewURL = attachment.linkPreviewURL { attachmentValue["linkPreviewUrl"] = linkPreviewURL @@ -169,7 +173,8 @@ public final class PublicChatMessage : NSObject { } // MARK: Convenience - @objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt, fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) { + @objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt, + fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) { guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() } let attachment = Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle) attachments.append(attachment) diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index 087fdd1e3..5c988246e 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -6,6 +6,8 @@ public protocol SessionMessagingKitStorageProtocol : SessionStore, PreKeyStore, func withAsync(_ work: (Any) -> Void, completion: () -> Void) func getUserPublicKey() -> String? + func getUserKeyPair() -> ECKeyPair? + func getUserDisplayName() -> String? func getOrGenerateRegistrationID(using transaction: Any) -> UInt32 func isClosedGroup(_ publicKey: String) -> Bool func getClosedGroupPrivateKey(for publicKey: String) -> String? @@ -13,4 +15,19 @@ public protocol SessionMessagingKitStorageProtocol : SessionStore, PreKeyStore, func markJobAsSucceeded(_ job: Job, using transaction: Any) func markJobAsFailed(_ job: Job, using transaction: Any) func getSenderCertificate(for publicKey: String) -> SMKSenderCertificate + func getAuthToken(for server: String) -> String? + func setAuthToken(for server: String, to newValue: String, using transaction: Any) + func removeAuthToken(for server: String, using transaction: Any) + func getOpenGroupPublicKey(for server: String) -> String? + func setOpenGroupPublicKey(for server: String, to newValue: String, using transaction: Any) + func getLastMessageServerID(for group: UInt64, on server: String) -> UInt? + 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) + func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64? + func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) + func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any) + func setUserCount(to newValue: Int, forOpenGroupWithID: String, using transaction: Any) + func getIDForMessage(withServerID serverID: UInt) -> UInt? + func setOpenGroupDisplayName(to displayName: String, for publicKey: String, on channel: UInt64, server: String, using transaction: Any) + func setLastProfilePictureUploadDate(_ date: Date) // Stored in user defaults so no transaction is needed } diff --git a/SessionMessagingKit/Utilities/AttachmentStream.swift b/SessionMessagingKit/Utilities/AttachmentStream.swift new file mode 100644 index 000000000..a1412886e --- /dev/null +++ b/SessionMessagingKit/Utilities/AttachmentStream.swift @@ -0,0 +1,11 @@ + +@objc public protocol AttachmentStream { + var encryptionKey: Data { get set } + var digest: Data { get set } + var serverId: UInt64 { get set } + var isUploaded: Bool { get set } + var downloadURL: String { get set } + + func readDataFromFile() throws -> Data + func save() +} diff --git a/SignalServiceKit/src/Loki/API/DotNetAPI.swift b/SessionMessagingKit/Utilities/DotNetAPI.swift similarity index 55% rename from SignalServiceKit/src/Loki/API/DotNetAPI.swift rename to SessionMessagingKit/Utilities/DotNetAPI.swift index 7fc906bfd..34f67d6d5 100644 --- a/SignalServiceKit/src/Loki/API/DotNetAPI.swift +++ b/SessionMessagingKit/Utilities/DotNetAPI.swift @@ -1,107 +1,100 @@ +import AFNetworking +import CryptoSwift import PromiseKit -import SessionMetadataKit +import SessionProtocolKit +import SessionSnodeKit +import SessionUtilitiesKit -/// Base class for `FileServerAPI` and `PublicChatAPI`. +/// Base class for `FileServerAPI` and `OpenGroupAPI`. public class DotNetAPI : NSObject { - internal static var userKeyPair: ECKeyPair { OWSIdentityManager.shared().identityKeyPair()! } - // MARK: Settings private static let attachmentType = "network.loki" private static let maxRetryCount: UInt = 4 // MARK: Error - @objc(LKDotNetAPIError) - public class DotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity - - @objc public static let generic = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ]) - @objc public static let parsingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ]) - @objc public static let signingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ]) - @objc public static let encryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ]) - @objc public static let decryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ]) - @objc public static let maxFileSizeExceeded = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ]) - } + public enum Error : LocalizedError { + case generic + case parsingFailed + case signingFailed + case encryptionFailed + case decryptionFailed + case maxFileSizeExceeded - // MARK: Storage - /// To be overridden by subclasses. - internal class var authTokenCollection: String { preconditionFailure("authTokenCollection is abstract and must be overridden.") } - - internal static func getAuthToken(for server: String) -> Promise { - if let token = getAuthTokenFromDatabase(for: server) { - return Promise.value(token) - } else { - return requestNewAuthToken(for: server).then2 { submitAuthToken($0, for: server) }.map2 { token in - Storage.writeSync { transaction in - setAuthToken(for: server, to: token, in: transaction) - } - return token + public var errorDescription: String? { + switch self { + case .generic: return "An error occurred." + case .parsingFailed: return "Invalid file server response." + case .signingFailed: return "Couldn't sign message." + case .encryptionFailed: return "Couldn't encrypt file." + case .decryptionFailed: return "Couldn't decrypt file." + case .maxFileSizeExceeded: return "Maximum file size exceeded." } } } - private static func getAuthTokenFromDatabase(for server: String) -> String? { - var result: String? = nil - Storage.read { transaction in - result = transaction.object(forKey: server, inCollection: authTokenCollection) as? String - } - return result - } - - private static func setAuthToken(for server: String, to newValue: String, in transaction: YapDatabaseReadWriteTransaction) { - transaction.setObject(newValue, forKey: server, inCollection: authTokenCollection) - } - - public static func removeAuthToken(for server: String) { - Storage.writeSync { transaction in - transaction.removeObject(forKey: server, inCollection: authTokenCollection) - } - } - // MARK: Lifecycle override private init() { } // MARK: Private API private static func requestNewAuthToken(for server: String) -> Promise { - print("[Loki] Requesting auth token for server: \(server).") - let queryParameters = "pubKey=\(getUserHexEncodedPublicKey())" + SNLog("Requesting auth token for server: \(server).") + guard let userKeyPair = Configuration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) } + let queryParameters = "pubKey=\(userKeyPair.publicKey().toHexString())" let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")! let request = TSRequest(url: url) - let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey) - : PublicChatAPI.getOpenGroupServerPublicKey(for: server) - return serverPublicKeyPromise.then2 { serverPublicKey in + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey) + : OpenGroupAPI.getOpenGroupServerPublicKey(for: server) + return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) - }.map2 { json in + }.map(on: DispatchQueue.global(qos: .userInitiated)) { json in guard let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String, let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else { - throw DotNetAPIError.parsingFailed + throw Error.parsingFailed } // Discard the "05" prefix if needed if serverPublicKey.count == 33 { let hexEncodedServerPublicKey = serverPublicKey.toHexString() - serverPublicKey = Data.data(fromHex: hexEncodedServerPublicKey.substring(from: 2))! + let startIndex = hexEncodedServerPublicKey.index(hexEncodedServerPublicKey.startIndex, offsetBy: 2) + serverPublicKey = Data.data(fromHex: String(hexEncodedServerPublicKey[startIndex.. Promise { - print("[Loki] Submitting auth token for server: \(server).") + SNLog("Submitting auth token for server: \(server).") let url = URL(string: "\(server)/loki/v1/submit_challenge")! - let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "token" : token ] + guard let userPublicKey = Configuration.shared.storage.getUserPublicKey() else { return Promise(error: Error.generic) } + let parameters = [ "pubKey" : userPublicKey, "token" : token ] let request = TSRequest(url: url, method: "POST", parameters: parameters) - let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey) - : PublicChatAPI.getOpenGroupServerPublicKey(for: server) - return serverPublicKeyPromise.then2 { serverPublicKey in + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey) + : OpenGroupAPI.getOpenGroupServerPublicKey(for: server) + return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) - }.map2 { _ in token } + }.map(on: DispatchQueue.global(qos: .userInitiated)) { _ in token } } // MARK: Public API + public static func getAuthToken(for server: String) -> Promise { + let storage = Configuration.shared.storage + if let token = storage.getAuthToken(for: server) { + return Promise.value(token) + } else { + return requestNewAuthToken(for: server).then(on: DispatchQueue.global(qos: .userInitiated)) { submitAuthToken($0, for: server) }.map(on: DispatchQueue.global(qos: .userInitiated)) { token in + storage.with { transaction in + storage.setAuthToken(for: server, to: token, using: transaction) + } + return token + } + } + } + @objc(downloadAttachmentFrom:) public static func objc_downloadAttachment(from url: String) -> AnyPromise { return AnyPromise.from(downloadAttachment(from: url)) @@ -119,17 +112,17 @@ public class DotNetAPI : NSObject { } let request = AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil, error: &error) if let error = error { - print("[Loki] Couldn't download attachment due to error: \(error).") + SNLog("Couldn't download attachment due to error: \(error).") return Promise(error: error) } - let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.fileServerPublicKey) - : PublicChatAPI.getOpenGroupServerPublicKey(for: host) - return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) { - serverPublicKeyPromise.then2 { serverPublicKey in - return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map2 { json in + let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.publicKey) + : OpenGroupAPI.getOpenGroupServerPublicKey(for: host) + return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) { + serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in + return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in guard let body = json["body"] as? JSON, let data = body["data"] as? [UInt8] else { - print("[Loki] Couldn't parse attachment from: \(json).") - throw DotNetAPIError.parsingFailed + SNLog("Couldn't parse attachment from: \(json).") + throw Error.parsingFailed } return Data(data) } @@ -138,27 +131,27 @@ public class DotNetAPI : NSObject { } @objc(uploadAttachment:withID:toServer:) - public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise { + public static func objc_uploadAttachment(_ attachment: AttachmentStream, with attachmentID: String, to server: String) -> AnyPromise { return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server)) } - public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise { + public static func uploadAttachment(_ attachment: AttachmentStream, with attachmentID: String, to server: String) -> Promise { let isEncryptionRequired = (server == FileServerAPI.server) return Promise() { seal in func proceed(with token: String) { // Get the attachment let data: Data guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else { - print("[Loki] Couldn't read attachment from disk.") - return seal.reject(DotNetAPIError.generic) + SNLog("Couldn't read attachment from disk.") + return seal.reject(Error.generic) } // Encrypt the attachment if needed if isEncryptionRequired { var encryptionKey = NSData() var digest = NSData() guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else { - print("[Loki] Couldn't encrypt attachment.") - return seal.reject(DotNetAPIError.encryptionFailed) + SNLog("Couldn't encrypt attachment.") + return seal.reject(Error.encryptionFailed) } attachment.encryptionKey = encryptionKey as Data attachment.digest = digest as Data @@ -167,36 +160,36 @@ public class DotNetAPI : NSObject { data = unencryptedAttachmentData } // Check the file size if needed - print("[Loki] File size: \(data.count) bytes.") + SNLog("File size: \(data.count) bytes.") if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier { - return seal.reject(DotNetAPIError.maxFileSizeExceeded) + return seal.reject(Error.maxFileSizeExceeded) } // Create the request let url = "\(server)/files" let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ] var error: NSError? - var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in + let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in let uuid = UUID().uuidString - print("[Loki] File UUID: \(uuid).") + SNLog("File UUID: \(uuid).") formData.appendPart(withFileData: data, name: "content", fileName: uuid, mimeType: "application/binary") }, error: &error) request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") if let error = error { - print("[Loki] Couldn't upload attachment due to error: \(error).") + SNLog("Couldn't upload attachment due to error: \(error).") return seal.reject(error) } // Send the request - let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.fileServerPublicKey) - : PublicChatAPI.getOpenGroupServerPublicKey(for: server) + let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey) + : OpenGroupAPI.getOpenGroupServerPublicKey(for: server) attachment.isUploaded = false attachment.save() - let _ = serverPublicKeyPromise.then2 { serverPublicKey in + let _ = serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey) - }.done2 { json in + }.done(on: DispatchQueue.global(qos: .userInitiated)) { json in // Parse the server ID & download URL guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else { - print("[Loki] Couldn't parse attachment from: \(json).") - return seal.reject(DotNetAPIError.parsingFailed) + SNLog("Couldn't parse attachment from: \(json).") + return seal.reject(Error.parsingFailed) } // Update the attachment attachment.serverId = serverID @@ -204,7 +197,7 @@ public class DotNetAPI : NSObject { attachment.downloadURL = downloadURL attachment.save() seal.fulfill(()) - }.catch2 { error in + }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in seal.reject(error) } } @@ -215,8 +208,8 @@ public class DotNetAPI : NSObject { } else { getAuthToken(for: server).done(on: DispatchQueue.global(qos: .userInitiated)) { token in proceed(with: token) - }.catch2 { error in - print("[Loki] Couldn't upload attachment due to error: \(error).") + }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in + SNLog("Couldn't upload attachment due to error: \(error).") seal.reject(error) } } diff --git a/SessionProtocolKit/Shared Sender Keys/SharedSenderKeys.swift b/SessionProtocolKit/Shared Sender Keys/SharedSenderKeys.swift index 600efe3ac..59725a6f7 100644 --- a/SessionProtocolKit/Shared Sender Keys/SharedSenderKeys.swift +++ b/SessionProtocolKit/Shared Sender Keys/SharedSenderKeys.swift @@ -49,7 +49,7 @@ public enum SharedSenderKeys { #endif guard let ratchet = Configuration.shared.storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: .current) else { let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) - print("[Loki] \(error.errorDescription!)") + SNLog("\(error.errorDescription!)") throw error } do { @@ -57,7 +57,7 @@ public enum SharedSenderKeys { Configuration.shared.storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: .current, using: transaction) return result } catch { - print("[Loki] Couldn't step ratchet due to error: \(error).") + SNLog("Couldn't step ratchet due to error: \(error).") throw error } } @@ -70,14 +70,14 @@ public enum SharedSenderKeys { let collection: ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current guard let ratchet = Configuration.shared.storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else { let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) - print("[Loki] \(error.errorDescription!)") + SNLog("\(error.errorDescription!)") throw error } if targetKeyIndex < ratchet.keyIndex { // There's no need to advance the ratchet if this is invoked for an old key index guard ratchet.messageKeys.count > targetKeyIndex else { let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) - print("[Loki] \(error.errorDescription!)") + SNLog("\(error.errorDescription!)") throw error } return ratchet @@ -89,7 +89,7 @@ public enum SharedSenderKeys { result = try step(result) currentKeyIndex = result.keyIndex } catch { - print("[Loki] Couldn't step ratchet due to error: \(error).") + SNLog("Couldn't step ratchet due to error: \(error).") throw error } } diff --git a/SessionProtocolKit/Signal/FallbackSessionCipher.swift b/SessionProtocolKit/Signal/FallbackSessionCipher.swift index eaad92248..8d3992744 100644 --- a/SessionProtocolKit/Signal/FallbackSessionCipher.swift +++ b/SessionProtocolKit/Signal/FallbackSessionCipher.swift @@ -35,7 +35,7 @@ import SessionUtilitiesKit do { return try DiffieHellman.encrypt(plaintext, using: symmetricKey) } catch { - print("[Loki] Couldn't encrypt message using fallback session cipher due to error: \(error).") + SNLog("Couldn't encrypt message using fallback session cipher due to error: \(error).") return nil } } @@ -45,7 +45,7 @@ import SessionUtilitiesKit do { return try DiffieHellman.decrypt(ivAndCiphertext, using: symmetricKey) } catch { - print("[Loki] Couldn't decrypt message using fallback session cipher due to error: \(error).") + SNLog("Couldn't decrypt message using fallback session cipher due to error: \(error).") return nil } } diff --git a/SessionProtocolKit/Signal/LokiSessionCipher.swift b/SessionProtocolKit/Signal/LokiSessionCipher.swift index 03b4ac18c..63bc02cf7 100644 --- a/SessionProtocolKit/Signal/LokiSessionCipher.swift +++ b/SessionProtocolKit/Signal/LokiSessionCipher.swift @@ -1,6 +1,6 @@ import Foundation -@objc(LKSessionCipher) +@objc(SNSessionCipher) public final class LokiSessionCipher : SessionCipher { private let sessionResetImplementation: SessionRestorationProtocol? private let sessionStore: SessionStore diff --git a/SessionProtocolKit/Signal/SessionRestorationProtocol.swift b/SessionProtocolKit/Signal/SessionRestorationProtocol.swift index f5518af94..9f73257d6 100644 --- a/SessionProtocolKit/Signal/SessionRestorationProtocol.swift +++ b/SessionProtocolKit/Signal/SessionRestorationProtocol.swift @@ -1,5 +1,5 @@ -@objc(LKSessionRestorationProtocol) +@objc(SNSessionRestorationProtocol) public protocol SessionRestorationProtocol { func validatePreKeyWhisperMessage(for publicKey: String, preKeyWhisperMessage: PreKeyWhisperMessage, using transaction: Any) throws diff --git a/SessionProtocolKit/Signal/SessionRestorationStatus.swift b/SessionProtocolKit/Signal/SessionRestorationStatus.swift index fa701f01f..3c6e0c199 100644 --- a/SessionProtocolKit/Signal/SessionRestorationStatus.swift +++ b/SessionProtocolKit/Signal/SessionRestorationStatus.swift @@ -1,5 +1,5 @@ -@objc(LKSessionRestorationStatus) +@objc(SNSessionRestorationStatus) public enum SessionRestorationStatus : Int { case none, initiated, requestReceived } diff --git a/SignalServiceKit/src/Loki/Utilities/AnyPromise+Conversion.swift b/SessionUtilitiesKit/AnyPromise+Conversion.swift similarity index 68% rename from SignalServiceKit/src/Loki/Utilities/AnyPromise+Conversion.swift rename to SessionUtilitiesKit/AnyPromise+Conversion.swift index 1c15fc554..eccf1bd7b 100644 --- a/SignalServiceKit/src/Loki/Utilities/AnyPromise+Conversion.swift +++ b/SessionUtilitiesKit/AnyPromise+Conversion.swift @@ -2,7 +2,7 @@ import PromiseKit public extension AnyPromise { - public static func from(_ promise: Promise) -> AnyPromise { + static func from(_ promise: Promise) -> AnyPromise { let result = AnyPromise(promise) result.retainUntilComplete() return result diff --git a/SessionUtilitiesKit/AnyPromise+Retaining.swift b/SessionUtilitiesKit/AnyPromise+Retaining.swift new file mode 100644 index 000000000..a708a85da --- /dev/null +++ b/SessionUtilitiesKit/AnyPromise+Retaining.swift @@ -0,0 +1,17 @@ +import PromiseKit + +public extension AnyPromise { + + /** + * Sometimes there isn't a straightforward candidate to retain a promise. In that case we tell the + * promise to self retain until it completes, to avoid the risk it's GC'd before completion. + */ + @objc + func retainUntilComplete() { + var retainCycle: AnyPromise? = self + _ = self.ensure { + assert(retainCycle != nil) + retainCycle = nil + } + } +} diff --git a/SessionUtilitiesKit/Mnemonic.swift b/SessionUtilitiesKit/Mnemonic.swift index 9cbaa1588..1786f5e72 100644 --- a/SessionUtilitiesKit/Mnemonic.swift +++ b/SessionUtilitiesKit/Mnemonic.swift @@ -144,7 +144,7 @@ private extension String { } } -@objc(LKMnemonic) +@objc(SNMnemonic) public final class ObjCMnemonic : NSObject { override private init() { } diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index b072d66ee..284cd5a58 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -654,6 +654,16 @@ C3A71D752558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */; }; C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; }; C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; }; + C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721342558BDF90043A11F /* OpenGroupMessage.swift */; }; + C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721352558BDF90043A11F /* OpenGroupAPI.swift */; }; + C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */; }; + C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721372558BDFA0043A11F /* OpenGroup.swift */; }; + C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */; }; + C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; }; + C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722292558C1E40043A11F /* DotNetAPI.swift */; }; + C3A7225E2558C38D0043A11F /* AnyPromise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */; }; + C3A722802558C4E10043A11F /* AttachmentStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7227F2558C4E10043A11F /* AttachmentStream.swift */; }; + C3A722922558C8940043A11F /* OpenGroupAPIDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */; }; C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; }; C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; }; C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.swift */; }; @@ -1693,6 +1703,16 @@ C3A71D722558A0F60043A11F /* SMKProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SMKProto.swift; path = Protos/SMKProto.swift; sourceTree = ""; }; C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSUnidentifiedDelivery.pb.swift; path = Protos/OWSUnidentifiedDelivery.pb.swift; sourceTree = ""; }; C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; + C3A721342558BDF90043A11F /* OpenGroupMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessage.swift; sourceTree = ""; }; + C3A721352558BDF90043A11F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = ""; }; + C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupInfo.swift; sourceTree = ""; }; + C3A721372558BDFA0043A11F /* OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = ""; }; + C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = ""; }; + C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = ""; }; + C3A722292558C1E40043A11F /* DotNetAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotNetAPI.swift; sourceTree = ""; }; + C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Retaining.swift"; sourceTree = ""; }; + C3A7227F2558C4E10043A11F /* AttachmentStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentStream.swift; sourceTree = ""; }; + C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIDelegate.swift; sourceTree = ""; }; C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = ""; }; C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = ""; }; C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; @@ -3377,9 +3397,31 @@ name = Metadata; sourceTree = ""; }; + C3A721332558BDDF0043A11F /* Open Groups */ = { + isa = PBXGroup; + children = ( + C3A721372558BDFA0043A11F /* OpenGroup.swift */, + C3A721352558BDF90043A11F /* OpenGroupAPI.swift */, + C3A722912558C8940043A11F /* OpenGroupAPIDelegate.swift */, + C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */, + C3A721342558BDF90043A11F /* OpenGroupMessage.swift */, + ); + path = "Open Groups"; + sourceTree = ""; + }; + C3A7215C2558C0AC0043A11F /* File Server */ = { + isa = PBXGroup; + children = ( + C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */, + ); + path = "File Server"; + sourceTree = ""; + }; C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( + C3A7227F2558C4E10043A11F /* AttachmentStream.swift */, + C3A722292558C1E40043A11F /* DotNetAPI.swift */, C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */, C3A71D0A2558989C0043A11F /* MessageWrapper.swift */, C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */, @@ -3432,6 +3474,8 @@ children = ( C3C2A68B255388D500C340D1 /* Meta */, C3C2A5D72553860B00C340D1 /* AESGCM.swift */, + C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */, + C3A7225D2558C38D0043A11F /* AnyPromise+Retaining.swift */, C3C2A5D12553860800C340D1 /* Array+Description.swift */, C3C2ABD12553C6C900C340D1 /* Data+SecureRandom.swift */, C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */, @@ -3473,6 +3517,8 @@ C300A5BB2554AFFB00555489 /* Messages */, C300A5F02554B08500555489 /* Sending & Receiving */, C352A2F325574B3300338F3E /* Jobs */, + C3A7215C2558C0AC0043A11F /* File Server */, + C3A721332558BDDF0043A11F /* Open Groups */, C3BBE0B32554F0D30050F1E3 /* Utilities */, ); path = SessionMessagingKit; @@ -5025,6 +5071,7 @@ C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */, C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */, C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, + C3A7225E2558C38D0043A11F /* AnyPromise+Retaining.swift in Sources */, C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */, C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, @@ -5034,6 +5081,7 @@ C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */, C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, + C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */, C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5043,6 +5091,7 @@ buildActionMask = 2147483647; files = ( C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */, + C3A722802558C4E10043A11F /* AttachmentStream.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */, C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */, @@ -5050,15 +5099,20 @@ C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, C352A3932557883D00338F3E /* JobDelegate.swift in Sources */, C352A31325574F5200338F3E /* MessageReceiveJob.swift in Sources */, + C3A722922558C8940043A11F /* OpenGroupAPIDelegate.swift in Sources */, C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, + C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */, C352A3892557876500338F3E /* JobQueue.swift in Sources */, C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */, + C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */, C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */, C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */, C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */, + C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, + C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */, @@ -5066,6 +5120,7 @@ C3471F4225553A4D00297E91 /* Threading.swift in Sources */, C300A5DD2554B06600555489 /* ClosedGroupUpdate.swift in Sources */, C3471FA42555439E00297E91 /* Notification+MessageSender.swift in Sources */, + C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */, C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */, C352A349255781F400338F3E /* AttachmentDownloadJob.swift in Sources */, C352A30925574D8500338F3E /* Message+Destination.swift in Sources */, @@ -5074,6 +5129,7 @@ C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */, C3C2A74425539EB700C340D1 /* Message.swift in Sources */, C300A5C92554B04E00555489 /* SessionRequest.swift in Sources */, + C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */, C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,