mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
commit
a19315cfeb
15 changed files with 397 additions and 211 deletions
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 960a820c76a95a64cfb1c6ad721c68f73a8f27b9
|
||||
Subproject commit 8b30c2d91fe7f9743350dd30521b5ca74e78766c
|
|
@ -8,6 +8,7 @@ extern NSString *const AppDelegateStoryboardMain;
|
|||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
- (void)stopLongPollerIfNeeded;
|
||||
- (void)createGroupChatsIfNeeded;
|
||||
- (void)createRSSFeedsIfNeeded;
|
||||
- (void)startGroupChatPollersIfNeeded;
|
||||
|
|
|
@ -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;
|
||||
|
@ -175,7 +178,7 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[DDLog flushLog];
|
||||
|
||||
[LKAPI stopLongPolling];
|
||||
[self stopLongPollerIfNeeded];
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application
|
||||
|
@ -194,7 +197,8 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[DDLog flushLog];
|
||||
|
||||
[LKAPI stopLongPolling];
|
||||
[self stopLongPollerIfNeeded];
|
||||
|
||||
if (self.lokiP2PServer) { [self.lokiP2PServer stop]; }
|
||||
}
|
||||
|
||||
|
@ -761,7 +765,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];
|
||||
|
@ -1359,8 +1363,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];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1406,23 +1410,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
|
||||
|
@ -1487,6 +1474,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<SSKProtoEnvelope *> *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).unsignedIntegerValue server:LKGroupChatAPI.publicChatServer displayName:NSLocalizedString(@"Loki Public Chat", @"") isDeletable:true];
|
||||
|
|
|
@ -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
|
||||
|
||||
@objc(initForGroup:)
|
||||
public init(for group: LokiGroupChat) {
|
||||
|
@ -44,6 +44,10 @@ public final class LokiGroupChatPoller : NSObject {
|
|||
x2.setTimestamp(message.timestamp)
|
||||
x2.setGroup(try! x1.build())
|
||||
x2.setBody(message.body)
|
||||
let messageServerID = message.serverID!
|
||||
let publicChatInfo = SSKProtoPublicChatInfo.builder()
|
||||
publicChatInfo.setServerID(messageServerID)
|
||||
x2.setPublicChatInfo(try! publicChatInfo.build())
|
||||
let x3 = SSKProtoContent.builder()
|
||||
x3.setDataMessage(try! x2.build())
|
||||
let x4 = SSKProtoEnvelope.builder(type: .ciphertext, timestamp: message.timestamp)
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -253,6 +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 = 999; // Loki: Internal public chat info
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
@ -421,3 +422,8 @@ message GroupDetails {
|
|||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
}
|
||||
|
||||
// Internal - DO NOT SEND
|
||||
message PublicChatInfo {
|
||||
optional uint64 serverID = 1;
|
||||
}
|
||||
|
|
|
@ -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<String>? {
|
||||
var result: Set<String>? = nil
|
||||
storage.dbReadConnection.read { transaction in
|
||||
result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set<String>?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func setReceivedMessageHashValues(to receivedMessageHashValues: Set<String>) {
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension Promise {
|
||||
|
||||
internal func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
|
||||
return recover() { error -> Promise<T> in
|
||||
switch error {
|
||||
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
|
||||
default: throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Void>]> in
|
||||
var promises = [Promise<Void>]()
|
||||
let connections = 3
|
||||
for i in 0..<connections {
|
||||
let (promise, cancel) = openConnection()
|
||||
promises.append(promise)
|
||||
cancels.append(cancel)
|
||||
}
|
||||
return when(resolved: promises)
|
||||
}.done { _ in
|
||||
// Since all promises are complete, we can clear the cancels
|
||||
cancelAllPromises()
|
||||
|
||||
// Keep long polling until it is stopped
|
||||
longPoll()
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
private static func cancelAllPromises() {
|
||||
cancels.forEach { cancel in cancel() }
|
||||
cancels.removeAll()
|
||||
}
|
||||
|
||||
private static func getUnusedSnodes() -> [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<Void>, cancel: Callback) {
|
||||
var isCancelled = false
|
||||
|
||||
let cancel = {
|
||||
isCancelled = true
|
||||
}
|
||||
|
||||
func connectToNextSnode() -> Promise<Void> {
|
||||
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<Void> {
|
||||
// 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<Void> 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<Void> 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)
|
||||
}
|
||||
}
|
|
@ -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<String>? {
|
||||
var result: Set<String>? = nil
|
||||
storage.dbReadConnection.read { transaction in
|
||||
result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set<String>?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set<String>) {
|
||||
storage.dbReadWriteConnection.readWrite { transaction in
|
||||
transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Error Handling
|
||||
private extension Promise {
|
||||
|
||||
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
|
||||
return recover() { error -> Promise<T> in
|
||||
switch error {
|
||||
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
|
||||
default: throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,13 @@ public final class LokiGroupChatAPI : NSObject {
|
|||
|
||||
// MARK: Error
|
||||
public enum Error : Swift.Error {
|
||||
case tokenParsingFailed, tokenDecryptionFailed, messageParsingFailed
|
||||
case tokenParsingFailed, tokenDecryptionFailed, messageParsingFailed, deletionParsingFailed
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -147,9 +147,7 @@ public final class LokiGroupChatAPI : NSObject {
|
|||
}
|
||||
guard hexEncodedPublicKey != userHexEncodedPublicKey else { 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)
|
||||
}
|
||||
}
|
||||
|
@ -186,22 +184,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
90
SignalServiceKit/src/Loki/API/LokiLongPoller.swift
Normal file
90
SignalServiceKit/src/Loki/API/LokiLongPoller.swift
Normal file
|
@ -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<Promise<Void>>()
|
||||
private var usedSnodes = Set<LokiAPITarget>()
|
||||
|
||||
// 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<Void>]> in
|
||||
guard let strongSelf = self else { return Guarantee.value([Result<Void>]()) }
|
||||
strongSelf.usedSnodes.removeAll()
|
||||
let connections: [Promise<Void>] = (0..<strongSelf.connectionCount).map { _ in
|
||||
let (promise, seal) = Promise<Void>.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<Void>) {
|
||||
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<Void>) -> Promise<Void> {
|
||||
return LokiAPI.getRawMessages(from: target, usingLongPolling: true).then { [weak self] rawResponse -> Promise<Void> 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1401,6 +1401,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: {
|
||||
|
|
|
@ -3308,6 +3308,9 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
if let _value = profile {
|
||||
builder.setProfile(_value)
|
||||
}
|
||||
if let _value = publicChatInfo {
|
||||
builder.setPublicChatInfo(_value)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@ -3379,6 +3382,10 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
proto.profile = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func setPublicChatInfo(_ valueParam: SSKProtoPublicChatInfo) {
|
||||
proto.publicChatInfo = valueParam.proto
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoDataMessage {
|
||||
return try SSKProtoDataMessage.parseProto(proto)
|
||||
}
|
||||
|
@ -3402,6 +3409,8 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
|
||||
@objc public let profile: SSKProtoDataMessageLokiProfile?
|
||||
|
||||
@objc public let publicChatInfo: SSKProtoPublicChatInfo?
|
||||
|
||||
@objc public var body: String? {
|
||||
guard proto.hasBody else {
|
||||
return nil
|
||||
|
@ -3449,7 +3458,8 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
quote: SSKProtoDataMessageQuote?,
|
||||
contact: [SSKProtoDataMessageContact],
|
||||
preview: [SSKProtoDataMessagePreview],
|
||||
profile: SSKProtoDataMessageLokiProfile?) {
|
||||
profile: SSKProtoDataMessageLokiProfile?,
|
||||
publicChatInfo: SSKProtoPublicChatInfo?) {
|
||||
self.proto = proto
|
||||
self.attachments = attachments
|
||||
self.group = group
|
||||
|
@ -3457,6 +3467,7 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
self.contact = contact
|
||||
self.preview = preview
|
||||
self.profile = profile
|
||||
self.publicChatInfo = publicChatInfo
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -3494,6 +3505,11 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
profile = try SSKProtoDataMessageLokiProfile.parseProto(proto.profile)
|
||||
}
|
||||
|
||||
var publicChatInfo: SSKProtoPublicChatInfo? = nil
|
||||
if proto.hasPublicChatInfo {
|
||||
publicChatInfo = try SSKProtoPublicChatInfo.parseProto(proto.publicChatInfo)
|
||||
}
|
||||
|
||||
// MARK: - Begin Validation Logic for SSKProtoDataMessage -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoDataMessage -
|
||||
|
@ -3504,7 +3520,8 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
quote: quote,
|
||||
contact: contact,
|
||||
preview: preview,
|
||||
profile: profile)
|
||||
profile: profile,
|
||||
publicChatInfo: publicChatInfo)
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -6215,3 +6232,94 @@ extension SSKProtoGroupDetails.SSKProtoGroupDetailsBuilder {
|
|||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoPublicChatInfo
|
||||
|
||||
@objc public class SSKProtoPublicChatInfo: NSObject {
|
||||
|
||||
// MARK: - SSKProtoPublicChatInfoBuilder
|
||||
|
||||
@objc public class func builder() -> SSKProtoPublicChatInfoBuilder {
|
||||
return SSKProtoPublicChatInfoBuilder()
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc public func asBuilder() -> SSKProtoPublicChatInfoBuilder {
|
||||
let builder = SSKProtoPublicChatInfoBuilder()
|
||||
if hasServerID {
|
||||
builder.setServerID(serverID)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
@objc public class SSKProtoPublicChatInfoBuilder: NSObject {
|
||||
|
||||
private var proto = SignalServiceProtos_PublicChatInfo()
|
||||
|
||||
@objc fileprivate override init() {}
|
||||
|
||||
@objc public func setServerID(_ valueParam: UInt64) {
|
||||
proto.serverID = valueParam
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoPublicChatInfo {
|
||||
return try SSKProtoPublicChatInfo.parseProto(proto)
|
||||
}
|
||||
|
||||
@objc public func buildSerializedData() throws -> Data {
|
||||
return try SSKProtoPublicChatInfo.parseProto(proto).serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let proto: SignalServiceProtos_PublicChatInfo
|
||||
|
||||
@objc public var serverID: UInt64 {
|
||||
return proto.serverID
|
||||
}
|
||||
@objc public var hasServerID: Bool {
|
||||
return proto.hasServerID
|
||||
}
|
||||
|
||||
private init(proto: SignalServiceProtos_PublicChatInfo) {
|
||||
self.proto = proto
|
||||
}
|
||||
|
||||
@objc
|
||||
public func serializedData() throws -> Data {
|
||||
return try self.proto.serializedData()
|
||||
}
|
||||
|
||||
@objc public class func parseData(_ serializedData: Data) throws -> SSKProtoPublicChatInfo {
|
||||
let proto = try SignalServiceProtos_PublicChatInfo(serializedData: serializedData)
|
||||
return try parseProto(proto)
|
||||
}
|
||||
|
||||
fileprivate class func parseProto(_ proto: SignalServiceProtos_PublicChatInfo) throws -> SSKProtoPublicChatInfo {
|
||||
// MARK: - Begin Validation Logic for SSKProtoPublicChatInfo -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoPublicChatInfo -
|
||||
|
||||
let result = SSKProtoPublicChatInfo(proto: proto)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public override var debugDescription: String {
|
||||
return "\(proto)"
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension SSKProtoPublicChatInfo {
|
||||
@objc public func serializedDataIgnoringErrors() -> Data? {
|
||||
return try! self.serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SSKProtoPublicChatInfo.SSKProtoPublicChatInfoBuilder {
|
||||
@objc public func buildIgnoringErrors() -> SSKProtoPublicChatInfo? {
|
||||
return try! self.build()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -802,6 +802,16 @@ struct SignalServiceProtos_DataMessage {
|
|||
/// Clears the value of `profile`. Subsequent reads from it will return its default value.
|
||||
mutating func clearProfile() {_uniqueStorage()._profile = nil}
|
||||
|
||||
/// Loki: Internal public chat info
|
||||
var publicChatInfo: SignalServiceProtos_PublicChatInfo {
|
||||
get {return _storage._publicChatInfo ?? SignalServiceProtos_PublicChatInfo()}
|
||||
set {_uniqueStorage()._publicChatInfo = newValue}
|
||||
}
|
||||
/// Returns true if `publicChatInfo` has been explicitly set.
|
||||
var hasPublicChatInfo: Bool {return _storage._publicChatInfo != nil}
|
||||
/// Clears the value of `publicChatInfo`. Subsequent reads from it will return its default value.
|
||||
mutating func clearPublicChatInfo() {_uniqueStorage()._publicChatInfo = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum Flags: SwiftProtobuf.Enum {
|
||||
|
@ -2492,6 +2502,28 @@ struct SignalServiceProtos_GroupDetails {
|
|||
fileprivate var _storage = _StorageClass.defaultInstance
|
||||
}
|
||||
|
||||
/// 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
|
||||
// methods supported on all messages.
|
||||
|
||||
var serverID: UInt64 {
|
||||
get {return _serverID ?? 0}
|
||||
set {_serverID = newValue}
|
||||
}
|
||||
/// Returns true if `serverID` has been explicitly set.
|
||||
var hasServerID: Bool {return self._serverID != nil}
|
||||
/// Clears the value of `serverID`. Subsequent reads from it will return its default value.
|
||||
mutating func clearServerID() {self._serverID = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _serverID: UInt64? = nil
|
||||
}
|
||||
|
||||
// MARK: - Code below here is support for the SwiftProtobuf runtime.
|
||||
|
||||
fileprivate let _protobuf_package = "SignalServiceProtos"
|
||||
|
@ -3146,6 +3178,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
9: .same(proto: "contact"),
|
||||
10: .same(proto: "preview"),
|
||||
101: .same(proto: "profile"),
|
||||
999: .same(proto: "publicChatInfo"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
|
@ -3160,6 +3193,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
var _contact: [SignalServiceProtos_DataMessage.Contact] = []
|
||||
var _preview: [SignalServiceProtos_DataMessage.Preview] = []
|
||||
var _profile: SignalServiceProtos_DataMessage.LokiProfile? = nil
|
||||
var _publicChatInfo: SignalServiceProtos_PublicChatInfo? = nil
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
|
@ -3177,6 +3211,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
_contact = source._contact
|
||||
_preview = source._preview
|
||||
_profile = source._profile
|
||||
_publicChatInfo = source._publicChatInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3203,6 +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 999: try decoder.decodeSingularMessageField(value: &_storage._publicChatInfo)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -3244,6 +3280,9 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
if let v = _storage._profile {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 101)
|
||||
}
|
||||
if let v = _storage._publicChatInfo {
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 999)
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
@ -3264,6 +3303,7 @@ extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf.
|
|||
if _storage._contact != rhs_storage._contact {return false}
|
||||
if _storage._preview != rhs_storage._preview {return false}
|
||||
if _storage._profile != rhs_storage._profile {return false}
|
||||
if _storage._publicChatInfo != rhs_storage._publicChatInfo {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
|
@ -5113,3 +5153,32 @@ extension SignalServiceProtos_GroupDetails.Avatar: SwiftProtobuf.Message, SwiftP
|
|||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularUInt64Field(value: &self._serverID)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._serverID {
|
||||
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SignalServiceProtos_PublicChatInfo, rhs: SignalServiceProtos_PublicChatInfo) -> Bool {
|
||||
if lhs._serverID != rhs._serverID {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue