diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index a60510de3..f47d88eae 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -27,14 +27,13 @@ public final class LokiAPI : NSObject { } internal static let workQueue = DispatchQueue(label: "LokiAPI.workQueue", qos: .userInitiated) - + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + internal static var userHexEncodedPublicKey: String { getUserHexEncodedPublicKey() } + /// All service node related errors must be handled on this queue to avoid race conditions maintaining e.g. failure counts. public static let errorHandlingQueue = DispatchQueue(label: "LokiAPI.errorHandlingQueue") - // MARK: Convenience - internal static let storage = OWSPrimaryStorage.shared() - internal static let userHexEncodedPublicKey = getUserHexEncodedPublicKey() - // MARK: Settings private static let useOnionRequests = true private static let maxRetryCount: UInt = 4 @@ -47,7 +46,7 @@ public final class LokiAPI : NSObject { public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000 public static let deviceLinkUpdateInterval: TimeInterval = 20 - // MARK: Types + // MARK: Nested Types public typealias RawResponse = Any @objc public class LokiAPIError : NSError { // Not called `Error` for Obj-C interoperablity @@ -104,12 +103,12 @@ public final class LokiAPI : NSObject { internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String, parameters: JSON, headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise { let url = URL(string: "\(target.address):\(target.port)/storage_rpc/v1")! - let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ]) - if let headers = headers { request.allHTTPHeaderFields = headers } - request.timeoutInterval = timeout ?? defaultTimeout if useOnionRequests { return OnionRequestAPI.sendOnionRequest(invoking: method, on: target, with: parameters, associatedWith: hexEncodedPublicKey).map { $0 as Any } } else { + let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ]) + if let headers = headers { request.allHTTPHeaderFields = headers } + request.timeoutInterval = timeout ?? defaultTimeout return TSNetworkManager.shared().perform(request, withCompletionQueue: workQueue) .map { $0.responseObject } .handlingSnodeErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey) @@ -133,7 +132,13 @@ public final class LokiAPI : NSObject { }.map { Set($0) } } } - + + @objc(getDestinationsFor:) + public static func objc_getDestinations(for hexEncodedPublicKey: String) -> AnyPromise { + let promise = getDestinations(for: hexEncodedPublicKey) + return AnyPromise.from(promise) + } + public static func getDestinations(for hexEncodedPublicKey: String) -> Promise<[Destination]> { var result: Promise<[Destination]>! storage.dbReadWriteConnection.readWrite { transaction in @@ -141,7 +146,13 @@ public final class LokiAPI : NSObject { } return result } - + + @objc(getDestinationsFor:inTransaction:) + public static func objc_getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { + let promise = getDestinations(for: hexEncodedPublicKey, in: transaction) + return AnyPromise.from(promise) + } + public static func getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<[Destination]> { let (promise, seal) = Promise<[Destination]>.pending() func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) { @@ -189,7 +200,13 @@ public final class LokiAPI : NSObject { } return promise } - + + @objc(sendSignalMessage:onP2PSuccess:) + public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> AnyPromise { + let promise = sendSignalMessage(signalMessage, onP2PSuccess: onP2PSuccess).mapValues { AnyPromise.from($0) }.map { Set($0) } + return AnyPromise.from(promise) + } + public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise> { guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) } let notificationCenter = NotificationCenter.default @@ -245,25 +262,6 @@ public final class LokiAPI : NSObject { } } - // MARK: Public API (Obj-C) - @objc(getDestinationsFor:) - public static func objc_getDestinations(for hexEncodedPublicKey: String) -> AnyPromise { - let promise = getDestinations(for: hexEncodedPublicKey) - return AnyPromise.from(promise) - } - - @objc(getDestinationsFor:inTransaction:) - public static func objc_getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise { - let promise = getDestinations(for: hexEncodedPublicKey, in: transaction) - return AnyPromise.from(promise) - } - - @objc(sendSignalMessage:onP2PSuccess:) - public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> AnyPromise { - let promise = sendSignalMessage(signalMessage, onP2PSuccess: onP2PSuccess).mapValues { AnyPromise.from($0) }.map { Set($0) } - return AnyPromise.from(promise) - } - // MARK: Parsing // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. diff --git a/SignalServiceKit/src/Loki/API/LokiAPITarget.swift b/SignalServiceKit/src/Loki/API/LokiAPITarget.swift index 6320a1fe2..1b5cff943 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPITarget.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPITarget.swift @@ -4,7 +4,7 @@ internal final class LokiAPITarget : NSObject, NSCoding { internal let port: UInt16 internal let publicKeySet: KeySet? - // MARK: Types + // MARK: Nested Types internal enum Method : String { /// Only supported by snode targets. case getSwarm = "get_snodes_for_pubkey" diff --git a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift index f02ad5985..0ef89ff4f 100644 --- a/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiDotNetAPI.swift @@ -3,11 +3,10 @@ import SignalMetadataKit /// Base class for `LokiFileServerAPI` and `LokiPublicChatAPI`. public class LokiDotNetAPI : NSObject { - - // MARK: Convenience - internal static let storage = OWSPrimaryStorage.shared() - internal static let userKeyPair = OWSIdentityManager.shared().identityKeyPair()! - internal static let userHexEncodedPublicKey = userKeyPair.hexEncodedPublicKey + + internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + internal static var userKeyPair: ECKeyPair { OWSIdentityManager.shared().identityKeyPair()! } + internal static var userHexEncodedPublicKey: String { userKeyPair.hexEncodedPublicKey } // MARK: Settings private static let attachmentType = "network.loki" @@ -69,7 +68,45 @@ public class LokiDotNetAPI : NSObject { // MARK: Lifecycle override private init() { } - // MARK: Attachments (Public API) + // MARK: Private API + private static func requestNewAuthToken(for server: String) -> Promise { + print("[Loki] Requesting auth token for server: \(server).") + let queryParameters = "pubKey=\(userHexEncodedPublicKey)" + let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")! + let request = TSRequest(url: url) + return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: LokiAPI.workQueue).map(on: LokiAPI.workQueue) { rawResponse in + guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String, + let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else { + throw LokiDotNetAPIError.parsingFailed + } + // Discard the "05" prefix if needed + if serverPublicKey.count == 33 { + let hexEncodedServerPublicKey = serverPublicKey.toHexString() + serverPublicKey = Data.data(fromHex: hexEncodedServerPublicKey.substring(from: 2))! + } + // The challenge is prefixed by the 16 bit IV + guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey), + let token = String(bytes: tokenAsData, encoding: .utf8) else { + throw LokiDotNetAPIError.decryptionFailed + } + return token + } + } + + private static func submitAuthToken(_ token: String, for server: String) -> Promise { + print("[Loki] Submitting auth token for server: \(server).") + let url = URL(string: "\(server)/loki/v1/submit_challenge")! + let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ] + let request = TSRequest(url: url, method: "POST", parameters: parameters) + return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: LokiAPI.workQueue).map { _ in token } + } + + // MARK: Public API + @objc(uploadAttachment:withID:toServer:) + public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, 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 { let isEncryptionRequired = (server == LokiFileServerAPI.server) return Promise() { seal in @@ -172,43 +209,4 @@ public class LokiDotNetAPI : NSObject { } } } - - // MARK: Private API - private static func requestNewAuthToken(for server: String) -> Promise { - print("[Loki] Requesting auth token for server: \(server).") - let queryParameters = "pubKey=\(userHexEncodedPublicKey)" - let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")! - let request = TSRequest(url: url) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: LokiAPI.workQueue).map(on: LokiAPI.workQueue) { rawResponse in - guard let json = rawResponse as? JSON, let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String, - let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else { - throw LokiDotNetAPIError.parsingFailed - } - // Discard the "05" prefix if needed - if serverPublicKey.count == 33 { - let hexEncodedServerPublicKey = serverPublicKey.toHexString() - serverPublicKey = Data.data(fromHex: hexEncodedServerPublicKey.substring(from: 2))! - } - // The challenge is prefixed by the 16 bit IV - guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey), - let token = String(bytes: tokenAsData, encoding: .utf8) else { - throw LokiDotNetAPIError.decryptionFailed - } - return token - } - } - - private static func submitAuthToken(_ token: String, for server: String) -> Promise { - print("[Loki] Submitting auth token for server: \(server).") - let url = URL(string: "\(server)/loki/v1/submit_challenge")! - let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ] - let request = TSRequest(url: url, method: "POST", parameters: parameters) - return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: LokiAPI.workQueue).map { _ in token } - } - - // MARK: Attachments (Public Obj-C API) - @objc(uploadAttachment:withID:toServer:) - public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise { - return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server)) - } } diff --git a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift index 1a8052e98..09f914bff 100644 --- a/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiFileServerAPI.swift @@ -16,7 +16,12 @@ public final class LokiFileServerAPI : LokiDotNetAPI { // MARK: Database override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } - // MARK: Device Links (Public API) + // MARK: Device Links + @objc(getDeviceLinksAssociatedWith:) + public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> AnyPromise { + return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKey)) + } + /// Gets the device links associated with the given hex encoded public key from the /// server and stores and returns the valid ones. public static func getDeviceLinks(associatedWith hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise> { @@ -138,13 +143,12 @@ public final class LokiFileServerAPI : LokiDotNetAPI { } } - // MARK: Device Links (Public Obj-C API) - @objc(getDeviceLinksAssociatedWith:) - public static func objc_getDeviceLinks(associatedWith hexEncodedPublicKey: String) -> AnyPromise { - return AnyPromise.from(getDeviceLinks(associatedWith: hexEncodedPublicKey)) + // MARK: Profile Pictures + @objc(uploadProfilePicture:) + public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise { + return AnyPromise.from(uploadProfilePicture(profilePicture)) } - - // MARK: Profile Pictures (Public API) + public static func uploadProfilePicture(_ profilePicture: Data) -> Promise { guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) } let url = "\(server)/files" @@ -168,10 +172,4 @@ public final class LokiFileServerAPI : LokiDotNetAPI { return downloadURL } } - - // MARK: Profile Pictures (Public Obj-C API) - @objc(uploadProfilePicture:) - public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise { - return AnyPromise.from(uploadProfilePicture(profilePicture)) - } } diff --git a/SignalServiceKit/src/Loki/API/Multi Device/DeviceLink.swift b/SignalServiceKit/src/Loki/API/Multi Device/DeviceLink.swift index d9a4325de..7241826a8 100644 --- a/SignalServiceKit/src/Loki/API/Multi Device/DeviceLink.swift +++ b/SignalServiceKit/src/Loki/API/Multi Device/DeviceLink.swift @@ -11,7 +11,7 @@ public final class DeviceLink : NSObject, NSCoding { return (userHexEncodedPublicKey == master.hexEncodedPublicKey) ? slave : master } - // MARK: Types + // MARK: Nested Types @objc(LKDevice) public final class Device : NSObject, NSCoding { @objc public let hexEncodedPublicKey: String diff --git a/SignalServiceKit/src/Loki/API/Public Chats/LokiPublicChatAPI.swift b/SignalServiceKit/src/Loki/API/Public Chats/LokiPublicChatAPI.swift index bea572af7..5ef51775d 100644 --- a/SignalServiceKit/src/Loki/API/Public Chats/LokiPublicChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/Public Chats/LokiPublicChatAPI.swift @@ -3,20 +3,20 @@ import PromiseKit @objc(LKPublicChatAPI) public final class LokiPublicChatAPI : LokiDotNetAPI { private static var moderators: [String:[UInt64:Set]] = [:] // Server URL to (channel ID to set of moderator IDs) + + @objc public static let defaultChats: [LokiPublicChat] = [] // Currently unused + public static var displayNameUpdatees: [String:Set] = [:] // MARK: Settings - private static let fallbackBatchCount = 64 - private static let maxRetryCount: UInt = 8 - - // MARK: Public Chat - private static let channelInfoType = "net.patter-app.settings" private static let attachmentType = "net.app.core.oembed" + private static let channelInfoType = "net.patter-app.settings" + private static let fallbackBatchCount = 64 + private static let maxRetryCount: UInt = 4 + public static let profilePictureType = "network.loki.messenger.avatar" @objc public static let publicChatMessageType = "network.loki.messenger.publicChat" - - @objc public static let defaultChats: [LokiPublicChat] = [] // Currently unused - + // MARK: Convenience private static var userDisplayName: String { return SSKEnvironment.shared.contactsManager.displayName(forPhoneIdentifier: userHexEncodedPublicKey) ?? "Anonymous" @@ -73,6 +73,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } // MARK: Public API + @objc(getMessagesForGroup:onServer:) + public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise { + return AnyPromise.from(getMessages(for: group, on: server)) + } + public static func getMessages(for channel: UInt64, on server: String) -> Promise<[LokiPublicChatMessage]> { var queryParameters = "include_annotations=1" if let lastMessageServerID = getLastMessageServerID(for: channel, on: server) { @@ -153,7 +158,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { }.sorted { $0.timestamp < $1.timestamp } } } - + + @objc(sendMessage:toGroup:onServer:) + public static func objc_sendMessage(_ message: LokiPublicChatMessage, to group: UInt64, on server: String) -> AnyPromise { + return AnyPromise.from(sendMessage(message, to: group, on: server)) + } + public static func sendMessage(_ message: LokiPublicChatMessage, 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() @@ -221,6 +231,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } } } + + @objc(deleteMessageWithID:forGroup:onServer:isSentByUser:) + public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise { + return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, isSentByUser: isSentByUser)) + } public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise { return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { @@ -281,7 +296,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } } } - + + @objc(getUserCountForGroup:onServer:) + public static func objc_getUserCount(for group: UInt64, on server: String) -> AnyPromise { + return AnyPromise.from(getUserCount(for: group, on: server)) + } + public static func getUserCount(for channel: UInt64, on server: String) -> Promise { return getAuthToken(for: server).then { token -> Promise in let queryParameters = "count=200" @@ -334,7 +354,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool { return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false } - + + @objc(setDisplayName:on:) + public static func objc_setDisplayName(to newDisplayName: String?, on server: String) -> AnyPromise { + return AnyPromise.from(setDisplayName(to: newDisplayName, on: server)) + } + public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise { print("[Loki] Updating display name on server: \(server).") return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { @@ -350,7 +375,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { } } } - + + @objc(setProfilePictureURL:usingProfileKey:on:) + public static func objc_setProfilePicture(to url: String?, using profileKey: Data, on server: String) -> AnyPromise { + return AnyPromise.from(setProfilePictureURL(to: url, using: profileKey, on: server)) + } + public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise { print("[Loki] Updating profile picture on server: \(server).") return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { @@ -387,46 +417,15 @@ public final class LokiPublicChatAPI : LokiDotNetAPI { return LokiPublicChatInfo(displayName: displayName) } } - + + @objc(reportMessageWithID:inChannel:onServer:) + public static func objc_reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> AnyPromise { + return AnyPromise.from(reportMessageWithID(messageID, in: channel, on: server)) + } + public static func reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> Promise { let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")! let request = TSRequest(url: url, method: "POST", parameters: [:]) return LokiFileServerProxy(for: server).perform(request).map { _ in } } - - // MARK: Public API (Obj-C) - @objc(getMessagesForGroup:onServer:) - public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise { - return AnyPromise.from(getMessages(for: group, on: server)) - } - - @objc(sendMessage:toGroup:onServer:) - public static func objc_sendMessage(_ message: LokiPublicChatMessage, to group: UInt64, on server: String) -> AnyPromise { - return AnyPromise.from(sendMessage(message, to: group, on: server)) - } - - @objc(deleteMessageWithID:forGroup:onServer:isSentByUser:) - public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise { - return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, isSentByUser: isSentByUser)) - } - - @objc(getUserCountForGroup:onServer:) - public static func objc_getUserCount(for group: UInt64, on server: String) -> AnyPromise { - return AnyPromise.from(getUserCount(for: group, on: server)) - } - - @objc(setDisplayName:on:) - public static func objc_setDisplayName(to newDisplayName: String?, on server: String) -> AnyPromise { - return AnyPromise.from(setDisplayName(to: newDisplayName, on: server)) - } - - @objc(setProfilePictureURL:usingProfileKey:on:) - public static func objc_setProfilePicture(to url: String?, using profileKey: Data, on server: String) -> AnyPromise { - return AnyPromise.from(setProfilePictureURL(to: url, using: profileKey, on: server)) - } - - @objc(reportMessageWithID:inChannel:onServer:) - public static func objc_reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> AnyPromise { - return AnyPromise.from(reportMessageWithID(messageID, in: channel, on: server)) - } }