diff --git a/Pods b/Pods index 960a820c7..8b30c2d91 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 960a820c76a95a64cfb1c6ad721c68f73a8f27b9 +Subproject commit 8b30c2d91fe7f9743350dd30521b5ca74e78766c diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme index f080c2943..dca83d632 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme @@ -28,7 +28,7 @@ buildForAnalyzing = "YES"> diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 2bddfb004..66492bb0b 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -30,7 +30,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.1.0 CFBundleSignature ???? CFBundleURLTypes @@ -47,7 +47,7 @@ CFBundleVersion - 11 + 14 ITSAppUsesNonExemptEncryption LOGS_EMAIL diff --git a/Signal/src/AppDelegate.h b/Signal/src/AppDelegate.h index 0730d665f..762c9df57 100644 --- a/Signal/src/AppDelegate.h +++ b/Signal/src/AppDelegate.h @@ -8,6 +8,7 @@ extern NSString *const AppDelegateStoryboardMain; @interface AppDelegate : UIResponder +- (void)stopLongPollerIfNeeded; - (void)createGroupChatsIfNeeded; - (void)createRSSFeedsIfNeeded; - (void)startGroupChatPollersIfNeeded; diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index e71538795..3eeec4b4c 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -63,7 +63,10 @@ static NSTimeInterval launchStartedAt; @property (nonatomic) BOOL hasInitialRootViewController; @property (nonatomic) BOOL areVersionMigrationsComplete; @property (nonatomic) BOOL didAppLaunchFail; + +// Loki @property (nonatomic) LKP2PServer *lokiP2PServer; +@property (nonatomic) LKLongPoller *lokiLongPoller; @property (nonatomic) LKGroupChatPoller *lokiPublicChatPoller; @property (nonatomic) LKRSSFeedPoller *lokiNewsFeedPoller; @property (nonatomic) LKRSSFeedPoller *lokiMessengerUpdatesFeedPoller; @@ -176,7 +179,7 @@ static NSTimeInterval launchStartedAt; [DDLog flushLog]; - [LKAPI stopLongPolling]; + [self stopLongPollerIfNeeded]; } - (void)applicationWillEnterForeground:(UIApplication *)application @@ -195,7 +198,8 @@ static NSTimeInterval launchStartedAt; [DDLog flushLog]; - [LKAPI stopLongPolling]; + [self stopLongPollerIfNeeded]; + if (self.lokiP2PServer) { [self.lokiP2PServer stop]; } } @@ -762,7 +766,7 @@ static NSTimeInterval launchStartedAt; [Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized]; // Loki: Start long polling - [LKAPI startLongPollingIfNeeded]; + [self startLongPollerIfNeeded]; // Loki: Tell our friends that we are online [LKP2PAPI broadcastOnlineStatus]; @@ -1360,8 +1364,8 @@ static NSTimeInterval launchStartedAt; // For non-legacy users, read receipts are on by default. [self.readReceiptManager setAreReadReceiptsEnabled:YES]; - // Start long polling - [LKAPI startLongPollingIfNeeded]; + // Loki: Start long polling + [self startLongPollerIfNeeded]; } } @@ -1407,23 +1411,6 @@ static NSTimeInterval launchStartedAt; [UIViewController attemptRotationToDeviceOrientation]; } -#pragma mark - Long polling - -- (void)handleNewMessagesReceived:(NSNotification *)notification -{ - NSArray *messages = (NSArray *)notification.userInfo[@"messages"]; - NSLog(@"[Loki] Received %lu messages through long polling.", messages.count); - - for (SSKProtoEnvelope *envelope in messages) { - NSData *envelopeData = [envelope serializedDataAndReturnError:nil]; - if (envelopeData != nil) { - [SSKEnvironment.shared.messageReceiver handleReceivedEnvelopeData:envelopeData]; - } else { - OWSFailDebug(@"Failed to deserialize envelope."); - } - } -} - #pragma mark - status bar touches - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event @@ -1488,6 +1475,34 @@ static NSTimeInterval launchStartedAt; #pragma mark - Loki +- (void)setUpLongPollerIfNeeded +{ + if (self.lokiLongPoller != nil) { return; } + NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; + if (userHexEncodedPublicKey == nil) { return; } + self.lokiLongPoller = [[LKLongPoller alloc] initOnMessagesReceived:^(NSArray *messages) { + for (SSKProtoEnvelope *message in messages) { + NSData *data = [message serializedDataAndReturnError:nil]; + if (data != nil) { + [SSKEnvironment.shared.messageReceiver handleReceivedEnvelopeData:data]; + } else { + NSLog(@"[Loki] Failed to deserialize envelope."); + } + } + }]; +} + +- (void)startLongPollerIfNeeded +{ + [self setUpLongPollerIfNeeded]; + [self.lokiLongPoller startIfNeeded]; +} + +- (void)stopLongPollerIfNeeded +{ + [self.lokiLongPoller stopIfNeeded]; +} + - (LKGroupChat *)lokiPublicChat { return [[LKGroupChat alloc] initWithServerID:LKGroupChatAPI.publicChatServerID server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true]; diff --git a/Signal/src/Loki/LokiGroupChatPoller.swift b/Signal/src/Loki/LokiGroupChatPoller.swift index d180a365d..1320769a8 100644 --- a/Signal/src/Loki/LokiGroupChatPoller.swift +++ b/Signal/src/Loki/LokiGroupChatPoller.swift @@ -7,7 +7,7 @@ public final class LokiGroupChatPoller : NSObject { private var hasStarted = false private let pollForNewMessagesInterval: TimeInterval = 4 - private let pollForDeletedMessagesInterval: TimeInterval = 32 * 60 + private let pollForDeletedMessagesInterval: TimeInterval = 20 private let storage = OWSPrimaryStorage.shared() private let ourHexEncodedPubKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey diff --git a/Signal/src/Loki/NewConversationViewController.swift b/Signal/src/Loki/NewConversationViewController.swift index a7e89efcb..f47c05e5c 100644 --- a/Signal/src/Loki/NewConversationViewController.swift +++ b/Signal/src/Loki/NewConversationViewController.swift @@ -105,6 +105,10 @@ final class NewConversationViewController : OWSViewController, OWSQRScannerDeleg let alert = UIAlertController(title: NSLocalizedString("Invalid Public Key", comment: ""), message: NSLocalizedString("Please check the public key you entered and try again.", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) + } else if OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey == hexEncodedPublicKey { + let alert = UIAlertController(title: NSLocalizedString("Can't Start Conversation", comment: ""), message: NSLocalizedString("Please enter the public key of the person you'd like to message.", comment: ""), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + presentAlert(alert) } else { let thread = TSContactThread.getOrCreateThread(contactId: hexEncodedPublicKey) SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false) diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 893354032..c1119fd2c 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -2,6 +2,7 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // +#import "AppDelegate.h" #import "AppSettingsViewController.h" #import "AboutTableViewController.h" #import "AdvancedSettingsTableViewController.h" @@ -533,7 +534,8 @@ [ThreadUtil deleteAllContent]; [SSKEnvironment.shared.identityManager clearIdentityKey]; [LKAPI clearRandomSnodePool]; - [LKAPI stopLongPolling]; + AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; + [appDelegate stopLongPollerIfNeeded]; [SSKEnvironment.shared.tsAccountManager resetForReregistration]; UIViewController *rootViewController = [[OnboardingController new] initialViewController]; OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController]; diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index a43bb6a5e..d92171b60 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -680,7 +680,8 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) { [ThreadUtil deleteAllContent]; [SSKEnvironment.shared.identityManager clearIdentityKey]; [LKAPI clearRandomSnodePool]; - [LKAPI stopLongPolling]; + AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate; + [appDelegate stopLongPollerIfNeeded]; [SSKEnvironment.shared.tsAccountManager resetForReregistration]; UIViewController *rootViewController = [[OnboardingController new] initialViewController]; OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController]; diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 94e785776..fe08c09f0 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2617,3 +2617,5 @@ "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\"." = "Scan the QR code of the person you'd like to securely message. They can find their QR code by going into Loki Messenger's in-app settings and clicking \"Show QR Code\"."; "Scan QR Code" = "Scan QR Code"; "Loki" = "Loki"; +"Can't Start Conversation" = "Can't Start Conversation"; +"Please enter the public key of the person you'd like to message." = "Please enter the public key of the person you'd like to message."; diff --git a/SignalMessaging/Info.plist b/SignalMessaging/Info.plist index 4c0d21863..a5ae6f942 100644 --- a/SignalMessaging/Info.plist +++ b/SignalMessaging/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0.0 + 1.1.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/SignalMessaging/environment/VersionMigrations.m b/SignalMessaging/environment/VersionMigrations.m index 143e4b0e0..3e12926bb 100644 --- a/SignalMessaging/environment/VersionMigrations.m +++ b/SignalMessaging/environment/VersionMigrations.m @@ -65,6 +65,7 @@ NS_ASSUME_NONNULL_BEGIN return; } + /* if ([self isVersion:previousVersion atLeast:@"1.0.2" andLessThan:@"2.0"]) { OWSLogError(@"Migrating from RedPhone no longer supported. Quitting."); // Not translating these as so few are affected. @@ -83,6 +84,7 @@ NS_ASSUME_NONNULL_BEGIN [CurrentAppContext().frontmostViewController presentAlert:alert]; } + */ if ([self isVersion:previousVersion atLeast:@"2.0.0" andLessThan:@"2.1.70"] && [self.tsAccountManager isRegistered]) { [self clearVideoCache]; diff --git a/SignalServiceKit/protobuf/SignalService.proto b/SignalServiceKit/protobuf/SignalService.proto index cd1e9c816..483fd5aad 100644 --- a/SignalServiceKit/protobuf/SignalService.proto +++ b/SignalServiceKit/protobuf/SignalService.proto @@ -253,7 +253,7 @@ message DataMessage { repeated Contact contact = 9; repeated Preview preview = 10; optional LokiProfile profile = 101; // Loki: The current user's profile - optional PublicChatInfo publicChatInfo = 900; // Loki: Internal public chat info + optional PublicChatInfo publicChatInfo = 999; // Loki: Internal public chat info } message NullMessage { @@ -423,7 +423,7 @@ message GroupDetails { optional bool blocked = 8; } -// Internal type - DO NOT SEND +// Internal - DO NOT SEND message PublicChatInfo { - optional uint64 serverId = 1; + optional uint64 serverID = 1; } diff --git a/SignalServiceKit/src/Account/TSAccountManager.m b/SignalServiceKit/src/Account/TSAccountManager.m index e968ffac3..ecce2ee27 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.m +++ b/SignalServiceKit/src/Account/TSAccountManager.m @@ -711,6 +711,8 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa return [AnyPromise promiseWithValue:@(1)]; } + return [AnyPromise promiseWithValue:@(1)]; + NSDate *_Nullable updateRequestDate = [self.dbConnection objectForKey:TSAccountManager_NeedsAccountAttributesUpdateKey inCollection:TSAccountManager_UserAccountCollection]; diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift deleted file mode 100644 index 0ebca88e4..000000000 --- a/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift +++ /dev/null @@ -1,49 +0,0 @@ -import PromiseKit - -internal extension LokiAPI { - - private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey" - private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection" - - internal static func getLastMessageHashValue(for target: LokiAPITarget) -> String? { - var result: String? = nil - // Uses a read/write connection because getting the last message hash value also removes expired messages as needed - // TODO: This shouldn't be the case; a getter shouldn't have an unexpected side effect - storage.dbReadWriteConnection.readWrite { transaction in - result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) - } - return result - } - - internal static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expirationDate: UInt64) { - storage.dbReadWriteConnection.readWrite { transaction in - storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expirationDate, transaction: transaction) - } - } - - internal static func getReceivedMessageHashValues() -> Set? { - var result: Set? = nil - storage.dbReadConnection.read { transaction in - result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set? - } - return result - } - - internal static func setReceivedMessageHashValues(to receivedMessageHashValues: Set) { - storage.dbReadWriteConnection.readWrite { transaction in - transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) - } - } -} - -internal extension Promise { - - internal func recoveringNetworkErrorsIfNeeded() -> Promise { - return recover() { error -> Promise in - switch error { - case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError - default: throw error - } - } - } -} diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift b/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift deleted file mode 100644 index 70554fd51..000000000 --- a/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift +++ /dev/null @@ -1,116 +0,0 @@ -import PromiseKit - -private typealias Callback = () -> Void - -public extension LokiAPI { - private static var isLongPolling = false - private static var shouldStopPolling = false - private static var usedSnodes = [LokiAPITarget]() - private static var cancels = [Callback]() - - /// Start long polling. - /// This will send a notification if new messages were received - @objc public static func startLongPollingIfNeeded() { - guard !isLongPolling else { return } - isLongPolling = true - shouldStopPolling = false - - print("[Loki] Started long polling.") - - longPoll() - } - - /// Stop long polling - @objc public static func stopLongPolling() { - shouldStopPolling = true - isLongPolling = false - usedSnodes.removeAll() - cancelAllPromises() - - print("[Loki] Stopped long polling.") - } - - /// The long polling loop - private static func longPoll() { - // This is here so we can stop the infinite loop - guard !shouldStopPolling else { return } - - getSwarm(for: userHexEncodedPublicKey).then { _ -> Guarantee<[Result]> in - var promises = [Promise]() - let connections = 3 - for i in 0.. [LokiAPITarget] { - let snodes = LokiAPI.swarmCache[userHexEncodedPublicKey] ?? [] - return snodes.filter { !usedSnodes.contains($0) } - } - - /// Open a connection to an unused snode and get messages from it - private static func openConnection() -> (Promise, cancel: Callback) { - var isCancelled = false - - let cancel = { - isCancelled = true - } - - func connectToNextSnode() -> Promise { - guard let nextSnode = getUnusedSnodes().first else { - // We don't have anymore unused snodes - return Promise.value(()) - } - - // Add the snode to the used array - usedSnodes.append(nextSnode) - - func getMessagesInfinitely(from target: LokiAPITarget) -> Promise { - // The only way to exit the infinite loop is to throw an error 3 times or cancel - return getRawMessages(from: target, usingLongPolling: true).then { rawResponse -> Promise in - // Check if we need to abort - guard !isCancelled else { throw PMKError.cancelled } - - // Process the messages - let messages = parseRawMessagesResponse(rawResponse, from: target) - - // Send our messages as a notification - NotificationCenter.default.post(name: .newMessagesReceived, object: nil, userInfo: ["messages": messages]) - - // Continue fetching if we haven't cancelled - return getMessagesInfinitely(from: target) - }.retryingIfNeeded(maxRetryCount: 3) - } - - // Keep getting messages for this snode - // If we errored out then connect to the next snode - return getMessagesInfinitely(from: nextSnode).recover { _ -> Promise in - // Cancelled, so just return successfully - guard !isCancelled else { return Promise.value(()) } - - // Connect to the next snode if we haven't cancelled - // We also need to remove the cached snode so we don't contact it again - dropIfNeeded(nextSnode, hexEncodedPublicKey: userHexEncodedPublicKey) - return connectToNextSnode() - } - } - - // Keep connecting to snodes - return (connectToNextSnode(), cancel) - } -} diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 04e74f481..c81a39589 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -173,4 +173,51 @@ public final class LokiAPI : NSObject { return envelope } } + + // MARK: Caching + private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey" + private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection" + + private static func getLastMessageHashValue(for target: LokiAPITarget) -> String? { + var result: String? = nil + // Uses a read/write connection because getting the last message hash value also removes expired messages as needed + // TODO: This shouldn't be the case; a getter shouldn't have an unexpected side effect + storage.dbReadWriteConnection.readWrite { transaction in + result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) + } + return result + } + + private static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expirationDate: UInt64) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expirationDate, transaction: transaction) + } + } + + private static func getReceivedMessageHashValues() -> Set? { + var result: Set? = nil + storage.dbReadConnection.read { transaction in + result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set? + } + return result + } + + private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set) { + storage.dbReadWriteConnection.readWrite { transaction in + transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) + } + } +} + +// MARK: Error Handling +private extension Promise { + + fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise { + return recover() { error -> Promise in + switch error { + case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError + default: throw error + } + } + } } diff --git a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift index 51b884777..ca612748c 100644 --- a/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiGroupChatAPI.swift @@ -28,13 +28,13 @@ public final class LokiGroupChatAPI : NSObject { // MARK: Error public enum Error : Swift.Error { - case tokenParsingFailed, tokenDecryptionFailed, messageParsingFailed, jsonParsingFailed + case tokenParsingFailed, tokenDecryptionFailed, messageParsingFailed, deletionParsingFailed, jsonParsingFailed } // MARK: Database private static let authTokenCollection = "LokiGroupChatAuthTokenCollection" private static let lastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection" - private static let firstMessageServerIDCollection = "LokiGroupChatFirstMessageServerIDCollection" + private static let lastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection" private static func getAuthTokenFromDatabase(for server: String) -> String? { var result: String? = nil @@ -64,17 +64,17 @@ public final class LokiGroupChatAPI : NSObject { } } - private static func getFirstMessageServerID(for group: UInt64, on server: String) -> UInt? { + private static func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt? { var result: UInt? = nil storage.dbReadConnection.read { transaction in - result = transaction.object(forKey: "\(server).\(group)", inCollection: firstMessageServerIDCollection) as! UInt? + result = transaction.object(forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) as! UInt? } return result } - private static func setFirstMessageServerID(for group: UInt64, on server: String, to newValue: UInt64) { + private static func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64) { storage.dbReadWriteConnection.readWrite { transaction in - transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: firstMessageServerIDCollection) + transaction.setObject(newValue, forKey: "\(server).\(group)", inCollection: lastDeletionServerIDCollection) } } @@ -146,9 +146,7 @@ public final class LokiGroupChatAPI : NSObject { return nil } let lastMessageServerID = getLastMessageServerID(for: group, on: server) - let firstMessageServerID = getFirstMessageServerID(for: group, on: server) if serverID > (lastMessageServerID ?? 0) { setLastMessageServerID(for: group, on: server, to: serverID) } - if serverID < (firstMessageServerID ?? UInt.max) { setFirstMessageServerID(for: group, on: server, to: serverID) } return LokiGroupMessage(serverID: serverID, hexEncodedPublicKey: hexEncodedPublicKey, displayName: displayName, body: body, type: publicChatMessageType, timestamp: timestamp) } } @@ -185,22 +183,27 @@ public final class LokiGroupChatAPI : NSObject { public static func getDeletedMessageServerIDs(for group: UInt64, on server: String) -> Promise<[UInt64]> { print("[Loki] Getting deleted messages for group chat with ID: \(group) on server: \(server).") - let firstMessageServerID = getFirstMessageServerID(for: group, on: server) ?? 0 - let queryParameters = "is_deleted=true&since_id=\(firstMessageServerID)" - let url = URL(string: "\(server)/channels/\(group)/messages?\(queryParameters)")! + let queryParameters: String + if let lastDeletionServerID = getLastDeletionServerID(for: group, on: server) { + queryParameters = "since_id=\(lastDeletionServerID)" + } else { + queryParameters = "count=\(fallbackBatchCount)" + } + let url = URL(string: "\(server)/loki/v1/channel/\(group)/deletes?\(queryParameters)")! let request = TSRequest(url: url) return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject }.map { rawResponse in - guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else { + guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else { print("[Loki] Couldn't parse deleted messages for group chat with ID: \(group) on server: \(server) from: \(rawResponse).") - throw Error.messageParsingFailed + throw Error.deletionParsingFailed } - return rawMessages.flatMap { message in - guard let serverID = message["id"] as? UInt64 else { - print("[Loki] Couldn't parse deleted message for group chat with ID: \(group) on server: \(server) from: \(message).") + return deletions.flatMap { 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 group chat with ID: \(group) on server: \(server) from: \(deletion).") return nil } - let isDeleted = (message["is_deleted"] as? Bool ?? false) - return isDeleted ? serverID : nil + let lastDeletionServerID = getLastDeletionServerID(for: group, on: server) + if serverID > (lastDeletionServerID ?? 0) { setLastDeletionServerID(for: group, on: server, to: serverID) } + return messageServerID } } } diff --git a/SignalServiceKit/src/Loki/API/LokiLongPoller.swift b/SignalServiceKit/src/Loki/API/LokiLongPoller.swift new file mode 100644 index 000000000..1acb6f7db --- /dev/null +++ b/SignalServiceKit/src/Loki/API/LokiLongPoller.swift @@ -0,0 +1,90 @@ +import PromiseKit + +@objc(LKLongPoller) +public final class LokiLongPoller : NSObject { + private let onMessagesReceived: ([SSKProtoEnvelope]) -> Void + private let storage = OWSPrimaryStorage.shared() + private var hasStarted = false + private var hasStopped = false + private var connections = Set>() + private var usedSnodes = Set() + + // MARK: Settings + private let connectionCount = 3 + private let retryInterval: TimeInterval = 4 + + // MARK: Convenience + private var userHexEncodedPublicKey: String { return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey } + + // MARK: Initialization + @objc public init(onMessagesReceived: @escaping ([SSKProtoEnvelope]) -> Void) { + self.onMessagesReceived = onMessagesReceived + super.init() + } + + // MARK: Public API + @objc public func startIfNeeded() { + guard !hasStarted else { return } + print("[Loki] Started long polling.") + hasStarted = true + hasStopped = false + openConnections() + } + + @objc public func stopIfNeeded() { + guard !hasStopped else { return } + print("[Loki] Stopped long polling.") + hasStarted = false + hasStopped = true + usedSnodes.removeAll() + } + + // MARK: Private API + private func openConnections() { + guard !hasStopped else { return } + LokiAPI.getSwarm(for: userHexEncodedPublicKey).then { [weak self] _ -> Guarantee<[Result]> in + guard let strongSelf = self else { return Guarantee.value([Result]()) } + strongSelf.usedSnodes.removeAll() + let connections: [Promise] = (0...pending() + strongSelf.openConnectionToNextSnode(seal: seal) + return promise + } + strongSelf.connections = Set(connections) + return when(resolved: connections) + }.ensure { [weak self] in + guard let strongSelf = self else { return } + Timer.scheduledTimer(withTimeInterval: strongSelf.retryInterval, repeats: false) { _ in + guard let strongSelf = self else { return } + strongSelf.openConnections() + } + } + } + + private func openConnectionToNextSnode(seal: Resolver) { + let swarm = LokiAPI.swarmCache[userHexEncodedPublicKey] ?? [] + let userHexEncodedPublicKey = self.userHexEncodedPublicKey + let unusedSnodes = Set(swarm).subtracting(usedSnodes) + if !unusedSnodes.isEmpty { + let nextSnode = unusedSnodes.randomElement()! + usedSnodes.insert(nextSnode) + print("[Loki] Opening long polling connection to \(nextSnode).") + longPoll(nextSnode, seal: seal).catch { [weak self] error in + print("[Loki] Long polling connection to \(nextSnode) failed; dropping it and switching to next snode.") + LokiAPI.dropIfNeeded(nextSnode, hexEncodedPublicKey: userHexEncodedPublicKey) + self?.openConnectionToNextSnode(seal: seal) + } + } else { + seal.fulfill(()) + } + } + + private func longPoll(_ target: LokiAPITarget, seal: Resolver) -> Promise { + return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then { [weak self] rawResponse -> Promise in + guard let strongSelf = self, !strongSelf.hasStopped else { return Promise.value(()) } + let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target) + strongSelf.onMessagesReceived(messages) + return strongSelf.longPoll(target, seal: seal) + } + } +} diff --git a/SignalServiceKit/src/Messages/OWSBlockingManager.m b/SignalServiceKit/src/Messages/OWSBlockingManager.m index 873193adf..b9d62960c 100644 --- a/SignalServiceKit/src/Messages/OWSBlockingManager.m +++ b/SignalServiceKit/src/Messages/OWSBlockingManager.m @@ -350,6 +350,7 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan // This method should only be called from within a synchronized block. - (void)syncBlockListIfNecessary { + /* OWSAssertDebug(_blockedPhoneNumberSet); // If we haven't yet successfully synced the current "block list" changes, @@ -376,6 +377,7 @@ NSString *const kOWSBlockingManager_SyncedBlockedGroupIdsKey = @"kOWSBlockingMan OWSLogInfo(@"retrying sync of block list"); [self sendBlockListSyncMessageWithPhoneNumbers:self.blockedPhoneNumbers groupIds:localBlockedGroupIds]; + */ } - (void)sendBlockListSyncMessageWithPhoneNumbers:(NSArray *)blockedPhoneNumbers diff --git a/SignalServiceKit/src/Messages/OWSMessageManager.m b/SignalServiceKit/src/Messages/OWSMessageManager.m index d8f3a4e9c..61289b259 100644 --- a/SignalServiceKit/src/Messages/OWSMessageManager.m +++ b/SignalServiceKit/src/Messages/OWSMessageManager.m @@ -1402,6 +1402,11 @@ NS_ASSUME_NONNULL_BEGIN thread:oldGroupThread envelope:envelope transaction:transaction]; + + if (dataMessage.publicChatInfo != nil && dataMessage.publicChatInfo.hasServerID) { + [self.primaryStorage setIDForMessageWithServerID:dataMessage.publicChatInfo.serverID to:incomingMessage.uniqueId in:transaction]; + } + return incomingMessage; } default: { diff --git a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m index 68214a7d7..7dc6d5c69 100644 --- a/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m +++ b/SignalServiceKit/src/Network/API/Requests/OWSRequestFactory.m @@ -220,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN NSString *path = [textSecureAccountsAPI stringByAppendingString:textSecureAttributesAPI]; NSString *authKey = self.tsAccountManager.serverAuthToken; - OWSAssertDebug(authKey.length > 0); + // OWSAssertDebug(authKey.length > 0); NSString *_Nullable pin = [self.ows2FAManager pinCode]; NSDictionary *accountAttributes = [self accountAttributesWithPin:pin authKey:authKey]; @@ -321,7 +321,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)accountAttributesWithPin:(nullable NSString *)pin authKey:(NSString *)authKey { - OWSAssertDebug(authKey.length > 0); + // OWSAssertDebug(authKey.length > 0); uint32_t registrationId = [self.tsAccountManager getOrGenerateRegistrationId]; BOOL isManualMessageFetchEnabled = self.tsAccountManager.isManualMessageFetchEnabled; diff --git a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift index 091118e4e..bf0fea786 100644 --- a/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift +++ b/SignalServiceKit/src/Protos/Generated/SignalService.pb.swift @@ -2502,7 +2502,7 @@ struct SignalServiceProtos_GroupDetails { fileprivate var _storage = _StorageClass.defaultInstance } -/// Internal type - DO NOT SEND +/// Internal - DO NOT SEND struct SignalServiceProtos_PublicChatInfo { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for @@ -3178,7 +3178,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. 9: .same(proto: "contact"), 10: .same(proto: "preview"), 101: .same(proto: "profile"), - 900: .same(proto: "publicChatInfo"), + 999: .same(proto: "publicChatInfo"), ] fileprivate class _StorageClass { @@ -3238,7 +3238,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. case 9: try decoder.decodeRepeatedMessageField(value: &_storage._contact) case 10: try decoder.decodeRepeatedMessageField(value: &_storage._preview) case 101: try decoder.decodeSingularMessageField(value: &_storage._profile) - case 900: try decoder.decodeSingularMessageField(value: &_storage._publicChatInfo) + case 999: try decoder.decodeSingularMessageField(value: &_storage._publicChatInfo) default: break } } @@ -3281,7 +3281,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf. try visitor.visitSingularMessageField(value: v, fieldNumber: 101) } if let v = _storage._publicChatInfo { - try visitor.visitSingularMessageField(value: v, fieldNumber: 900) + try visitor.visitSingularMessageField(value: v, fieldNumber: 999) } } try unknownFields.traverse(visitor: &visitor) @@ -5157,7 +5157,7 @@ extension SignalServiceProtos_GroupDetails.Avatar: SwiftProtobuf.Message, SwiftP extension SignalServiceProtos_PublicChatInfo: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { static let protoMessageName: String = _protobuf_package + ".PublicChatInfo" static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ - 1: .same(proto: "serverId"), + 1: .same(proto: "serverID"), ] mutating func decodeMessage(decoder: inout D) throws { diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist index cbb6f2007..065a126aa 100644 --- a/SignalShareExtension/Info.plist +++ b/SignalShareExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.0.0 + 1.1.0 CFBundleVersion - 11 + 14 ITSAppUsesNonExemptEncryption NSAppTransportSecurity diff --git a/privacy-policy.md b/privacy-policy.md new file mode 100644 index 000000000..851cc6c97 --- /dev/null +++ b/privacy-policy.md @@ -0,0 +1,53 @@ + +## Privacy Policy + +Loki Messenger is a client application that interacts with the “Service Node” network, which is not wholly owned or run by LAG Foundation Ltd (the publishers of Loki Messenger). This privacy policy can only apply between the user and LAG Foundation Ltd and is limited to any information that LAG Foundation Ltd collects by providing the Loki Messenger client application. Your messages are always encrypted when they travel over the internet, so they can never be shared or viewed (in plaintext) by anyone but yourself and the intended recipients. + +### Information you provide + +Messages. LAG Foundation and the Service Node network cannot decrypt or otherwise access the content of your messages. The Service Node network queues end-to-end encrypted messages on its servers for delivery to devices that are temporarily offline (e.g. a phone whose battery has died) or can not otherwise be reached. Your message history is stored on your own device or devices. + +User Support. If you contact any member of the Loki Messenger team, any personal data you may share with us is kept only for the purposes of researching the issue and contacting you about your case. + +### Information that is or may be automatically collected + +Messages. Due to the decentralised nature of the Loki Messenger, your encrypted messages will be stored on the Service Node network. Although no one, including the Service Nodes, can see the contents of the messages, in beta non Lokinet integrated Loki Messenger your metadata may be collected by Service Nodes on the network. This may include who messages are being sent to, who they are being sent by, IP addresses, and public key information. No personally identifying information is included in messages, however if Service Node operators are able to identify which is your specific public key (account number) through some other means (for instance, by posting it on Twitter), they may be able to link you to your Messenger identity. LAG Foundation Ltd is only responsible for a small subset of the Service Node network and can not guarantee that your metadata is not being tracked when using the Beta Loki Messenger version. In this version of the Loki Messenger, there is limited metadata privacy protection. We will ensure that this is made clear in the app, until an update which includes much stronger protections is released. + + + +Usage Information. the Loki Messenger beta may automatically send us usage information periodically. All personally identifying information is stripped from this data that you provide to us. This data is critical to us to understand how we can improve the Loki Messenger by understanding how users interact with the app, and what is going wrong when the app stops working. This data includes: how often you use the app, how many people you are talking to, how many messages you are sending, how long you spend in the app each day, and any error messages that the app encounters. We do not collect your IP address, public key, contact list, conversation history, or any other type of personal information. When the Full version of Loki messenger is released this data collection will become opt in. + + + +You are able to audit how this data is collected in the app by visiting [https://github.com/loki-project/loki-messenger-ios](https://github.com/loki-project/loki-messenger-ios) and inspecting the code of our analytics module. + +### Information we may share + +Instances where LAG Foundation Ltd may need to share your data: + +- To meet any applicable law, regulation, legal process or enforceable governmental request. + +- To enforce applicable Terms, including investigation of potential violations. + +- To detect, prevent, or otherwise address fraud, security, or technical issues. + +- To protect against harm to the rights, property, or safety of LAG Foundation Ltd, our users, or the public as required or permitted by law. + + +Note: The LAG can only share the data it has obtained, which is very minimal even in the beta program, when Loki Messenger is released live such data will be anonymised before collection limiting the usefulness of such data for purposes other than analysing our broad userbase. + +Third Parties + +LAG Foundation Ltd distributes the Loki messenger iOS application through two primary sources, the Apple Store and directly through Github. These third party service providers may also collect data on your download of and usage of Loki Messenger. This data collection is our of our hands, although it is information we can view and use as stated in this policy. For more information about how these platforms collect and use information, you should read their privacy policies prior to using them. + +You may opt out of this data collection by building the iOS binaries from the repository [https://github.com/loki-project/loki-messenger-ios](https://github.com/loki-project/loki-messenger-ios) and sideloading the app onto a modified Apple device. + +### Updates + +We will update this privacy policy as needed so that it is current, accurate, and as clear as possible. Your continued use of our Services confirms your acceptance of our updated Privacy Policy. + +### Contact Us + +If you have questions about our Privacy Policy please contact us at team@loki.network. + +Last Updated 28/08/2019