mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'dev' of https://github.com/loki-project/loki-messenger-ios into sync-closed-group
This commit is contained in:
commit
63aafe06a1
49 changed files with 520 additions and 338 deletions
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit eeaffe766824af6c6b7e7351da6bddb1e9d99611
|
||||
Subproject commit 2870e676deec6a7ddb931edb6f0284f1f5b36085
|
32
README.md
32
README.md
|
@ -1,18 +1,28 @@
|
|||
# Session iOS
|
||||
|
||||
Session is a fully end-to-end encrypted messenger that aims to remove any chance of metadata collection by routing all messages through an onion routing network. Storage of offline messages is handled by Service Nodes, which are a distributed set of nodes run by the community.
|
||||
[Download Session on the App Store](https://getsession.org/iphone)
|
||||
|
||||
## Contributing Code
|
||||
Code should be contributed via pull request to the master branch, all submitted code will be reviewed.
|
||||
## Summary
|
||||
|
||||
## Contributing Ideas
|
||||
Have something you want to say about Session? Create an issue with the discussion tag and the community can weigh in.
|
||||
Session integrates directly with [Loki Service Nodes](https://lokidocs.com/ServiceNodes/SNOverview/), which are a set of distributed, decentralized and Sybil resistant nodes. Service Nodes act as servers which store messages offline, and a set of nodes which allow for onion routing functionality obfuscating users' IP addresses. For a full understanding of how Session works, read the [Session Whitepaper](https://getsession.org/whitepaper).
|
||||
|
||||
## Cryptography Notice
|
||||
![iOSSession](https://i.imgur.com/vM62EJm.png)
|
||||
|
||||
This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software.
|
||||
BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted.
|
||||
See <http://www.wassenaar.org/> for more information.
|
||||
|
||||
The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms.
|
||||
The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code.
|
||||
## Want to Contribute? Found a Bug or Have a feature request?
|
||||
|
||||
Please search for any [existing issues](https://github.com/loki-project/session-ios/issues) that describe your bugs in order to avoid duplicate submissions. Submissions can be made by making a pull request to our development branch. If you don't know where to start contributing , try reading the Github issues page for ideas.
|
||||
|
||||
## Build instruction
|
||||
|
||||
Build instructions can be found in [BUILDING.md](BUILDING.md).
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2011 Whisper Systems
|
||||
|
||||
Copyright 2013-2017 Open Whisper Systems
|
||||
|
||||
Copyright 2019-2020 The Loki Project
|
||||
|
||||
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
|
|
@ -4230,7 +4230,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
|
||||
|
@ -4244,7 +4244,7 @@
|
|||
INFOPLIST_FILE = SignalShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -4292,7 +4292,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
|
@ -4311,7 +4311,7 @@
|
|||
INFOPLIST_FILE = SignalShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@ -4346,7 +4346,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
|
@ -4365,7 +4365,7 @@
|
|||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
@ -4415,7 +4415,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
|
@ -4439,7 +4439,7 @@
|
|||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.utilities";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
|
@ -4624,7 +4624,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -4659,7 +4659,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
@ -4691,7 +4691,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Signal/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 45;
|
||||
CURRENT_PROJECT_VERSION = 46;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -4726,7 +4726,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 1.0.3;
|
||||
MARKETING_VERSION = 1.0.4;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<key>BuildDetails</key>
|
||||
<dict>
|
||||
<key>CarthageVersion</key>
|
||||
<string>0.34.0</string>
|
||||
<string>0.33.0</string>
|
||||
<key>OSXVersion</key>
|
||||
<string>10.15.2</string>
|
||||
<key>WebRTCCommit</key>
|
||||
|
|
|
@ -11,6 +11,8 @@ extern NSString *const AppDelegateStoryboardMain;
|
|||
- (void)startLongPollerIfNeeded;
|
||||
- (void)stopLongPollerIfNeeded;
|
||||
- (void)setUpDefaultPublicChatsIfNeeded;
|
||||
- (void)startOpenGroupPollersIfNeeded;
|
||||
- (void)stopOpenGroupPollersIfNeeded;
|
||||
- (void)createRSSFeedsIfNeeded;
|
||||
- (void)startRSSFeedPollersIfNeeded;
|
||||
|
||||
|
|
|
@ -177,7 +177,9 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[DDLog flushLog];
|
||||
|
||||
// Loki: Stop pollers
|
||||
[self stopLongPollerIfNeeded];
|
||||
[self stopOpenGroupPollersIfNeeded];
|
||||
}
|
||||
|
||||
- (void)applicationWillEnterForeground:(UIApplication *)application
|
||||
|
@ -195,8 +197,10 @@ static NSTimeInterval launchStartedAt;
|
|||
OWSLogInfo(@"applicationWillTerminate.");
|
||||
|
||||
[DDLog flushLog];
|
||||
|
||||
|
||||
// Loki: Stop pollers
|
||||
[self stopLongPollerIfNeeded];
|
||||
[self stopOpenGroupPollersIfNeeded];
|
||||
|
||||
if (self.lokiP2PServer) { [self.lokiP2PServer stop]; }
|
||||
}
|
||||
|
@ -316,9 +320,6 @@ static NSTimeInterval launchStartedAt;
|
|||
name:NSNotificationName_2FAStateDidChange
|
||||
object:nil];
|
||||
|
||||
// Loki - Observe new messages received notifications
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleNewMessagesReceived:) name:NSNotification.newMessagesReceived object:nil];
|
||||
|
||||
// Loki - Observe thread deleted notifications
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(handleThreadDeleted:) name:NSNotification.threadDeleted object:nil];
|
||||
|
||||
|
@ -770,14 +771,32 @@ static NSTimeInterval launchStartedAt;
|
|||
[self.socketManager requestSocketOpen];
|
||||
[Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized];
|
||||
|
||||
NSString *userHexEncodedPublicKey = self.tsAccountManager.localNumber;
|
||||
|
||||
// Loki: Tell our friends that we are online
|
||||
[LKP2PAPI broadcastOnlineStatus];
|
||||
|
||||
// Loki: Start long polling
|
||||
// Loki: Start pollers
|
||||
[self startLongPollerIfNeeded];
|
||||
[self startOpenGroupPollersIfNeeded];
|
||||
|
||||
// Loki: Get device links
|
||||
[LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
|
||||
[[LKFileServerAPI getDeviceLinksAssociatedWith:userHexEncodedPublicKey] retainUntilComplete];
|
||||
|
||||
// Loki: Update profile picture if needed
|
||||
NSUserDefaults *userDefaults = NSUserDefaults.standardUserDefaults;
|
||||
NSDate *now = [NSDate new];
|
||||
NSDate *lastProfilePictureUpload = (NSDate *)[userDefaults objectForKey:@"lastProfilePictureUpload"];
|
||||
if (lastProfilePictureUpload != nil && [now timeIntervalSinceDate:lastProfilePictureUpload] > 14 * 24 * 60 * 60) {
|
||||
OWSProfileManager *profileManager = OWSProfileManager.sharedManager;
|
||||
NSString *displayName = [profileManager profileNameForRecipientId:userHexEncodedPublicKey];
|
||||
UIImage *profilePicture = [profileManager profileAvatarForRecipientId:userHexEncodedPublicKey];
|
||||
[profileManager updateLocalProfileName:displayName avatarImage:profilePicture success:^{
|
||||
// Do nothing; the user defaults flag is updated in LokiFileServerAPI
|
||||
} failure:^(NSError *error) {
|
||||
// Do nothing
|
||||
} requiresSync:YES];
|
||||
}
|
||||
|
||||
if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) {
|
||||
OWSLogInfo(@"Retrying to register for remote notifications since user hasn't registered yet.");
|
||||
|
@ -1118,6 +1137,8 @@ static NSTimeInterval launchStartedAt;
|
|||
OWSLogInfo(@"Ignoring remote notification; app not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentAppContext().wasWokenUpBySilentPushNotification = true;
|
||||
|
||||
[LKLogger print:@"[Loki] Silent push notification received; fetching messages."];
|
||||
|
||||
|
@ -1135,7 +1156,7 @@ static NSTimeInterval launchStartedAt;
|
|||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
publicChats = [LKDatabaseUtilities getAllPublicChats:transaction];
|
||||
}];
|
||||
for (LKPublicChat *publicChat in publicChats) {
|
||||
for (LKPublicChat *publicChat in publicChats.allValues) {
|
||||
if (![publicChat isKindOfClass:LKPublicChat.class]) { continue; } // For some reason publicChat is sometimes a base 64 encoded string...
|
||||
LKPublicChatPoller *poller = [[LKPublicChatPoller alloc] initForPublicChat:publicChat];
|
||||
[poller stop];
|
||||
|
@ -1146,8 +1167,12 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
PMKJoin(promises).then(^(id results) {
|
||||
completionHandler(UIBackgroundFetchResultNewData);
|
||||
CurrentAppContext().wasWokenUpBySilentPushNotification = false;
|
||||
[LKLogger print:@"[Loki] UIBackgroundFetchResultNewData"];
|
||||
}).catch(^(id error) {
|
||||
completionHandler(UIBackgroundFetchResultFailed);
|
||||
CurrentAppContext().wasWokenUpBySilentPushNotification = false;
|
||||
[LKLogger print:@"[Loki] UIBackgroundFetchResultFailed"];
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1444,11 +1469,12 @@ static NSTimeInterval launchStartedAt;
|
|||
// Loki: Start friend request expiration job
|
||||
[self.lokiFriendRequestExpirationJob startIfNecessary];
|
||||
|
||||
// Loki: Start long polling
|
||||
// Loki: Start pollers
|
||||
[self startLongPollerIfNeeded];
|
||||
[self startOpenGroupPollersIfNeeded];
|
||||
|
||||
// Loki: Get device links
|
||||
[LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber];
|
||||
[[LKFileServerAPI getDeviceLinksAssociatedWith:self.tsAccountManager.localNumber] retainUntilComplete]; // TODO: Is this even needed?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1586,16 +1612,6 @@ static NSTimeInterval launchStartedAt;
|
|||
[self.lokiLongPoller stopIfNeeded];
|
||||
}
|
||||
|
||||
- (LKRSSFeed *)lokiNewsFeed
|
||||
{
|
||||
return [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:@"Loki News" isDeletable:true];
|
||||
}
|
||||
|
||||
- (LKRSSFeed *)lokiMessengerUpdatesFeed
|
||||
{
|
||||
return [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:@"Session Updates" isDeletable:false];
|
||||
}
|
||||
|
||||
- (void)setUpDefaultPublicChatsIfNeeded
|
||||
{
|
||||
for (LKPublicChat *chat in LKPublicChatAPI.defaultChats) {
|
||||
|
@ -1612,6 +1628,27 @@ static NSTimeInterval launchStartedAt;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)startOpenGroupPollersIfNeeded
|
||||
{
|
||||
[LKPublicChatManager.shared startPollersIfNeeded];
|
||||
[SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
|
||||
}
|
||||
|
||||
- (void)stopOpenGroupPollersIfNeeded
|
||||
{
|
||||
[LKPublicChatManager.shared stopPollers];
|
||||
}
|
||||
|
||||
- (LKRSSFeed *)lokiNewsFeed
|
||||
{
|
||||
return [[LKRSSFeed alloc] initWithId:@"loki.network.feed" server:@"https://loki.network/feed/" displayName:@"Loki News" isDeletable:true];
|
||||
}
|
||||
|
||||
- (LKRSSFeed *)lokiMessengerUpdatesFeed
|
||||
{
|
||||
return [[LKRSSFeed alloc] initWithId:@"loki.network.messenger-updates.feed" server:@"https://loki.network/category/messenger-updates/feed/" displayName:@"Session Updates" isDeletable:false];
|
||||
}
|
||||
|
||||
- (void)createRSSFeedsIfNeeded
|
||||
{
|
||||
NSArray *feeds = @[ /*self.lokiNewsFeed,*/ self.lokiMessengerUpdatesFeed ];
|
||||
|
@ -1670,6 +1707,7 @@ static NSTimeInterval launchStartedAt;
|
|||
[SSKEnvironment.shared.identityManager clearIdentityKey];
|
||||
[LKAPI clearRandomSnodePool];
|
||||
[self stopLongPollerIfNeeded];
|
||||
[self stopOpenGroupPollersIfNeeded];
|
||||
[self.lokiNewsFeedPoller stop];
|
||||
[self.lokiMessengerUpdatesFeedPoller stop];
|
||||
[LKPublicChatManager.shared stopPollers];
|
||||
|
|
|
@ -7,8 +7,8 @@ final class ConversationTitleView : UIView {
|
|||
// MARK: Types
|
||||
private enum Status : Int {
|
||||
case calculatingPoW = 1
|
||||
case contactingNetwork = 2
|
||||
case sendingMessage = 3
|
||||
case routing = 2
|
||||
case messageSending = 3
|
||||
case messageSent = 4
|
||||
case messageFailed = 5
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ final class ConversationTitleView : UIView {
|
|||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.addObserver(self, selector: #selector(handleProfileChangedNotification(_:)), name: NSNotification.Name(rawValue: kNSNotificationName_OtherUsersProfileDidChange), object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleCalculatingPoWNotification(_:)), name: .calculatingPoW, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleContactingNetworkNotification(_:)), name: .contactingNetwork, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleSendingMessageNotification(_:)), name: .sendingMessage, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleRoutingNotification(_:)), name: .routing, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSendingNotification(_:)), name: .messageSending, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentNotification(_:)), name: .messageSent, object: nil)
|
||||
notificationCenter.addObserver(self, selector: #selector(handleMessageFailedNotification(_:)), name: .messageFailed, object: nil)
|
||||
}
|
||||
|
@ -99,14 +99,14 @@ final class ConversationTitleView : UIView {
|
|||
setStatusIfNeeded(to: .calculatingPoW, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleContactingNetworkNotification(_ notification: Notification) {
|
||||
@objc private func handleRoutingNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .contactingNetwork, forMessageWithTimestamp: timestamp)
|
||||
setStatusIfNeeded(to: .routing, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleSendingMessageNotification(_ notification: Notification) {
|
||||
@objc private func handleMessageSendingNotification(_ notification: Notification) {
|
||||
guard let timestamp = notification.object as? NSNumber else { return }
|
||||
setStatusIfNeeded(to: .sendingMessage, forMessageWithTimestamp: timestamp)
|
||||
setStatusIfNeeded(to: .messageSending, forMessageWithTimestamp: timestamp)
|
||||
}
|
||||
|
||||
@objc private func handleMessageSentNotification(_ notification: Notification) {
|
||||
|
@ -147,8 +147,8 @@ final class ConversationTitleView : UIView {
|
|||
self.subtitleLabel.isHidden = false
|
||||
switch self.currentStatus {
|
||||
case .calculatingPoW: self.subtitleLabel.text = NSLocalizedString("Encrypting message", comment: "")
|
||||
case .contactingNetwork: self.subtitleLabel.text = NSLocalizedString("Tracing a path", comment: "")
|
||||
case .sendingMessage: self.subtitleLabel.text = NSLocalizedString("Sending message", comment: "")
|
||||
case .routing: self.subtitleLabel.text = NSLocalizedString("Tracing a path", comment: "")
|
||||
case .messageSending: self.subtitleLabel.text = NSLocalizedString("Sending message", comment: "")
|
||||
case .messageSent: self.subtitleLabel.text = NSLocalizedString("Message sent securely", comment: "")
|
||||
case .messageFailed: self.subtitleLabel.text = NSLocalizedString("Message failed to send", comment: "")
|
||||
case nil:
|
||||
|
|
|
@ -12,8 +12,9 @@ final class LokiPushNotificationManager : NSObject {
|
|||
@objc(registerWithToken:)
|
||||
func register(with token: Data) {
|
||||
let hexEncodedToken = token.map { String(format: "%02.2hhx", $0) }.joined()
|
||||
let oldToken = UserDefaults.standard.string(forKey: "deviceToken")
|
||||
let lastUploadTime = UserDefaults.standard.double(forKey: "lastDeviceTokenUploadTime")
|
||||
let userDefaults = UserDefaults.standard
|
||||
let oldToken = userDefaults[.deviceToken]
|
||||
let lastUploadTime = userDefaults[.lastDeviceTokenUpload]
|
||||
let now = Date().timeIntervalSince1970
|
||||
if hexEncodedToken == oldToken && now - lastUploadTime < 2 * 24 * 60 * 60 {
|
||||
print("[Loki] Device token hasn't changed; no need to upload.")
|
||||
|
@ -21,7 +22,11 @@ final class LokiPushNotificationManager : NSObject {
|
|||
}
|
||||
// Send token to Loki server
|
||||
let parameters = [ "token" : hexEncodedToken ]
|
||||
#if DEBUG
|
||||
let url = URL(string: "https://dev.apns.getsession.org/register")!
|
||||
#else
|
||||
let url = URL(string: "https://live.apns.getsession.org/register")!
|
||||
#endif
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ]
|
||||
TSNetworkManager.shared().makeRequest(request, success: { _, response in
|
||||
|
@ -29,8 +34,8 @@ final class LokiPushNotificationManager : NSObject {
|
|||
guard json["code"] as? Int != 0 else {
|
||||
return print("[Loki] An error occured during device token registration: \(json["message"] as? String ?? "nil").")
|
||||
}
|
||||
UserDefaults.standard.set(hexEncodedToken, forKey: "deviceToken")
|
||||
UserDefaults.standard.set(now, forKey: "lastDeviceTokenUploadTime")
|
||||
userDefaults[.deviceToken] = hexEncodedToken
|
||||
userDefaults[.lastDeviceTokenUpload] = now
|
||||
}, failure: { _, error in
|
||||
print("[Loki] Couldn't register device token.")
|
||||
})
|
||||
|
|
|
@ -92,7 +92,7 @@ final class DeviceNameModal : Modal {
|
|||
@objc private func changeName() {
|
||||
let name = nameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
if !name.isEmpty {
|
||||
UserDefaults.standard.set(name, forKey: "\(device.hexEncodedPublicKey)_display_name")
|
||||
UserDefaults.standard[.slaveDeviceName(device.hexEncodedPublicKey)] = name
|
||||
delegate?.handleDeviceNameChanged(to: name, for: device)
|
||||
} else {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Error", comment: ""), message: NSLocalizedString("Please pick a name", comment: ""), preferredStyle: .alert)
|
||||
|
|
|
@ -88,8 +88,9 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
|
|||
titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
// Set up seed reminder view if needed
|
||||
let hasViewedSeed = UserDefaults.standard.bool(forKey: "hasViewedSeed")
|
||||
let isMasterDevice = (UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") == nil)
|
||||
let userDefaults = UserDefaults.standard
|
||||
let hasViewedSeed = userDefaults[.hasViewedSeed]
|
||||
let isMasterDevice = userDefaults.isMasterDevice
|
||||
if !hasViewedSeed && isMasterDevice {
|
||||
view.addSubview(seedReminderView)
|
||||
seedReminderView.pin(.leading, to: .leading, of: view)
|
||||
|
@ -134,8 +135,8 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
|
|||
if OWSIdentityManager.shared().identityKeyPair() != nil {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.setUpDefaultPublicChatsIfNeeded()
|
||||
appDelegate.startOpenGroupPollersIfNeeded()
|
||||
appDelegate.createRSSFeedsIfNeeded()
|
||||
LokiPublicChatManager.shared.startPollersIfNeeded()
|
||||
appDelegate.startRSSFeedPollersIfNeeded()
|
||||
}
|
||||
// Do initial update
|
||||
|
@ -145,14 +146,14 @@ final class HomeVC : UIViewController, UITableViewDataSource, UITableViewDelegat
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
isViewVisible = true
|
||||
// let hasSeenOpenGroupSuggestionSheet = UserDefaults.standard.bool(forKey: "hasSeenOpenGroupSuggestionSheet")
|
||||
// let hasSeenOpenGroupSuggestionSheet = UserDefaults.standard[.hasSeenOpenGroupSuggestionSheet]
|
||||
// if !hasSeenOpenGroupSuggestionSheet {
|
||||
// let openGroupSuggestionSheet = OpenGroupSuggestionSheet()
|
||||
// openGroupSuggestionSheet.modalPresentationStyle = .overFullScreen
|
||||
// openGroupSuggestionSheet.modalTransitionStyle = .crossDissolve
|
||||
// present(openGroupSuggestionSheet, animated: true, completion: nil)
|
||||
// }
|
||||
UserDefaults.standard.set(true, forKey: "hasLaunchedOnce")
|
||||
UserDefaults.standard[.hasLaunchedOnce] = true
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
|
|
|
@ -100,7 +100,7 @@ final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingMod
|
|||
mainStackView.pin(to: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Show device unlinked alert if needed
|
||||
if UserDefaults.standard.bool(forKey: "wasUnlinked") {
|
||||
if UserDefaults.standard[.wasUnlinked] {
|
||||
let alert = UIAlertController(title: NSLocalizedString("Device Unlinked", comment: ""), message: NSLocalizedString("Your device was unlinked successfully", comment: ""), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
|
@ -190,8 +190,7 @@ final class LandingVC : UIViewController, LinkDeviceVCDelegate, DeviceLinkingMod
|
|||
}
|
||||
|
||||
func handleDeviceLinkAuthorized(_ deviceLink: DeviceLink) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
userDefaults.set(deviceLink.master.hexEncodedPublicKey, forKey: "masterDeviceHexEncodedPublicKey")
|
||||
UserDefaults.standard[.masterHexEncodedPublicKey] = deviceLink.master.hexEncodedPublicKey
|
||||
fakeChatViewContentOffset = fakeChatView.contentOffset
|
||||
DispatchQueue.main.async {
|
||||
self.fakeChatView.contentOffset = self.fakeChatViewContentOffset
|
||||
|
|
|
@ -186,15 +186,15 @@ final class NewClosedGroupVC : UIViewController, UITableViewDataSource, UITableV
|
|||
DispatchQueue.main.async {
|
||||
SSKEnvironment.shared.messageSender.send(message, success: {
|
||||
DispatchQueue.main.async {
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
}, failure: { error in
|
||||
let message = TSErrorMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, failedMessageType: .groupCreationFailed)
|
||||
message.save()
|
||||
DispatchQueue.main.async {
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ private final class EnterPublicKeyVC : UIViewController {
|
|||
weak var newPrivateChatVC: NewPrivateChatVC!
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
|
|
|
@ -71,7 +71,7 @@ final class OpenGroupSuggestionSheet : Sheet {
|
|||
}
|
||||
|
||||
override func close() {
|
||||
UserDefaults.standard.set(true, forKey: "hasSeenOpenGroupSuggestionSheet")
|
||||
UserDefaults.standard[.hasSeenOpenGroupSuggestionSheet] = true
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
private var bottomConstraint: NSLayoutConstraint!
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
|
|
|
@ -187,7 +187,7 @@ final class RegisterVC : UIViewController {
|
|||
databaseConnection.setObject(keyPair!, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair!.hexEncodedPublicKey
|
||||
OWSPrimaryStorage.shared().setRestorationTime(0)
|
||||
UserDefaults.standard.set(false, forKey: "hasViewedSeed")
|
||||
UserDefaults.standard[.hasViewedSeed] = false
|
||||
let displayNameVC = DisplayNameVC()
|
||||
navigationController!.pushViewController(displayNameVC, animated: true)
|
||||
}
|
||||
|
|
|
@ -178,7 +178,7 @@ final class RestoreVC : UIViewController {
|
|||
databaseConnection.setObject(keyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = keyPair.hexEncodedPublicKey
|
||||
OWSPrimaryStorage.shared().setRestorationTime(Date().timeIntervalSince1970)
|
||||
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
|
||||
UserDefaults.standard[.hasViewedSeed] = true
|
||||
mnemonicTextField.resignFirstResponder()
|
||||
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in
|
||||
let displayNameVC = DisplayNameVC()
|
||||
|
|
|
@ -62,7 +62,7 @@ final class SeedModal : Modal {
|
|||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.largeSpacing)
|
||||
// Mark seed as viewed
|
||||
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
|
||||
UserDefaults.standard[.hasViewedSeed] = true
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -182,7 +182,7 @@ final class SeedVC : UIViewController {
|
|||
self.seedReminderView.subtitle = NSLocalizedString("Make sure to store your recovery phrase in a safe place", comment: "")
|
||||
}, completion: nil)
|
||||
seedReminderView.setProgress(1, animated: true)
|
||||
UserDefaults.standard.set(true, forKey: "hasViewedSeed")
|
||||
UserDefaults.standard[.hasViewedSeed] = true
|
||||
NotificationCenter.default.post(name: .seedViewed, object: nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
|
|||
private var isEditingDisplayName = false { didSet { handleIsEditingDisplayNameChanged() } }
|
||||
|
||||
private lazy var userHexEncodedPublicKey: String = {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") {
|
||||
if let masterHexEncodedPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] {
|
||||
return masterHexEncodedPublicKey
|
||||
} else {
|
||||
return getUserHexEncodedPublicKey()
|
||||
|
@ -180,7 +180,7 @@ final class SettingsVC : UIViewController, AvatarViewHelperDelegate {
|
|||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("Notifications", comment: ""), color: Colors.text, action: #selector(showNotificationSettings))
|
||||
]
|
||||
let isMasterDevice = (UserDefaults.standard.string(forKey: "masterDeviceHexEncodedPublicKey") == nil)
|
||||
let isMasterDevice = UserDefaults.standard.isMasterDevice
|
||||
if isMasterDevice {
|
||||
result.append(getSeparator())
|
||||
result.append(getSettingButton(withTitle: NSLocalizedString("Devices", comment: ""), color: Colors.text, action: #selector(showLinkedDevices)))
|
||||
|
|
|
@ -429,12 +429,12 @@ typedef enum : NSUInteger {
|
|||
name:NSNotification.calculatingPoW
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleContactingNetworkNotification:)
|
||||
name:NSNotification.contactingNetwork
|
||||
selector:@selector(handleRoutingNotification:)
|
||||
name:NSNotification.routing
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleSendingMessageNotification:)
|
||||
name:NSNotification.sendingMessage
|
||||
selector:@selector(handleMessageSendingNotification:)
|
||||
name:NSNotification.messageSending
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(handleMessageSentNotification:)
|
||||
|
@ -5419,13 +5419,13 @@ typedef enum : NSUInteger {
|
|||
[self setProgressIfNeededTo:0.25f forMessageWithTimestamp:timestamp];
|
||||
}
|
||||
|
||||
- (void)handleContactingNetworkNotification:(NSNotification *)notification
|
||||
- (void)handleRoutingNotification:(NSNotification *)notification
|
||||
{
|
||||
NSNumber *timestamp = (NSNumber *)notification.object;
|
||||
[self setProgressIfNeededTo:0.50f forMessageWithTimestamp:timestamp];
|
||||
}
|
||||
|
||||
- (void)handleSendingMessageNotification:(NSNotification *)notification
|
||||
- (void)handleMessageSendingNotification:(NSNotification *)notification
|
||||
{
|
||||
NSNumber *timestamp = (NSNumber *)notification.object;
|
||||
[self setProgressIfNeededTo:0.75f forMessageWithTimestamp:timestamp];
|
||||
|
|
|
@ -682,6 +682,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
|
|||
[LKAPI clearRandomSnodePool];
|
||||
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
|
||||
[appDelegate stopLongPollerIfNeeded];
|
||||
[appDelegate stopOpenGroupPollersIfNeeded];
|
||||
[SSKEnvironment.shared.tsAccountManager resetForReregistration];
|
||||
UIViewController *rootViewController = [[OnboardingController new] initialViewController];
|
||||
OWSNavigationController *navigationController = [[OWSNavigationController alloc] initWithRootViewController:rootViewController];
|
||||
|
|
|
@ -28,6 +28,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
|
|||
|
||||
@synthesize mainWindow = _mainWindow;
|
||||
@synthesize appLaunchTime = _appLaunchTime;
|
||||
@synthesize wasWokenUpBySilentPushNotification = _wasWokenUpBySilentPushNotification;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
|
@ -40,6 +41,7 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
|
|||
self.reportedApplicationState = UIApplicationStateInactive;
|
||||
|
||||
_appLaunchTime = [NSDate new];
|
||||
_wasWokenUpBySilentPushNotification = false;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationWillEnterForeground:)
|
||||
|
|
|
@ -416,10 +416,10 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
|
|||
NSData *encryptedAvatarData = [self encryptProfileData:avatarData profileKey:newProfileKey];
|
||||
OWSAssertDebug(encryptedAvatarData.length > 0);
|
||||
|
||||
[[LKFileServerAPI setProfilePicture:encryptedAvatarData]
|
||||
.thenOn(dispatch_get_main_queue(), ^(NSString *url) {
|
||||
[[LKFileServerAPI uploadProfilePicture:encryptedAvatarData]
|
||||
.thenOn(dispatch_get_main_queue(), ^(NSString *downloadURL) {
|
||||
[self.localUserProfile updateWithProfileKey:newProfileKey dbConnection:self.dbConnection completion:^{
|
||||
successBlock(url);
|
||||
successBlock(downloadURL);
|
||||
}];
|
||||
})
|
||||
.catchOn(dispatch_get_main_queue(), ^(id result) {
|
||||
|
|
|
@ -39,7 +39,7 @@ public class RefreshPreKeysOperation: OWSOperation {
|
|||
// Loki: Doing this on the global queue to match Signal
|
||||
DispatchQueue.global().async {
|
||||
guard self.primaryStorage.currentSignedPrekeyId() == nil else {
|
||||
print("[Loki] Using existing signed pre key.")
|
||||
print("[Loki] Skipping pre key refresh; using existing signed pre key.")
|
||||
return self.reportSuccess()
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import PromiseKit
|
||||
|
||||
public extension LokiAPI {
|
||||
|
||||
|
||||
/// Only ever accessed from `LokiAPI.errorHandlingQueue` to avoid race conditions.
|
||||
fileprivate static var failureCount: [LokiAPITarget:UInt] = [:]
|
||||
|
||||
// MARK: Settings
|
||||
private static let minimumSnodeCount = 2
|
||||
private static let targetSnodeCount = 3
|
||||
private static let maxRandomSnodePoolSize = 1024
|
||||
fileprivate static let failureThreshold = 2
|
||||
|
||||
// MARK: Caching
|
||||
internal static var swarmCache: [String:[LokiAPITarget]] = [:]
|
||||
private static let swarmCacheKey = "swarmCacheKey"
|
||||
private static let swarmCacheCollection = "swarmCacheCollection"
|
||||
|
||||
internal static func dropIfNeeded(_ target: LokiAPITarget, hexEncodedPublicKey: String) {
|
||||
let swarm = LokiAPI.swarmCache[hexEncodedPublicKey]
|
||||
|
@ -39,7 +39,7 @@ public extension LokiAPI {
|
|||
"method" : "get_n_service_nodes",
|
||||
"params" : [
|
||||
"active_only" : true,
|
||||
"limit" : 24,
|
||||
"limit" : maxRandomSnodePoolSize,
|
||||
"fields" : [
|
||||
"public_ip" : true,
|
||||
"storage_port" : true,
|
||||
|
@ -49,7 +49,7 @@ public extension LokiAPI {
|
|||
]
|
||||
])
|
||||
print("[Loki] Invoking get_n_service_nodes on \(target).")
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { intermediate in
|
||||
return TSNetworkManager.shared().perform(request).map(on: DispatchQueue.global()) { intermediate in
|
||||
let rawResponse = intermediate.responseObject
|
||||
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
|
||||
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
|
||||
|
@ -59,13 +59,15 @@ public extension LokiAPI {
|
|||
}
|
||||
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(idKey: idKey, encryptionKey: encryptionKey))
|
||||
})
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
return randomSnodePool.randomElement()!
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiAPITarget> in
|
||||
}.recover { error -> Promise<LokiAPITarget> in
|
||||
print("[Loki] Failed to contact seed node at: \(target).")
|
||||
throw error
|
||||
}.retryingIfNeeded(maxRetryCount: 16) // The seed nodes have historically been unreliable
|
||||
} else {
|
||||
return Promise<LokiAPITarget> { seal in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
seal.fulfill(randomSnodePool.randomElement()!)
|
||||
}
|
||||
}
|
||||
|
@ -76,6 +78,7 @@ public extension LokiAPI {
|
|||
return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) }
|
||||
} else {
|
||||
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
|
||||
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
|
||||
return getRandomSnode().then(on: DispatchQueue.global()) { invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,26 @@ import PromiseKit
|
|||
|
||||
@objc(LKAPI)
|
||||
public final class LokiAPI : NSObject {
|
||||
/// Only ever modified from the message processing queue (`OWSBatchMessageProcessor.processingQueue`).
|
||||
private static var syncMessageTimestamps: [String:Set<UInt64>] = [:]
|
||||
public static var lastDeviceLinkUpdate: [String:Date] = [:] // Hex encoded public key to date
|
||||
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] = [:] // Thread ID to set of user hex encoded public keys
|
||||
|
||||
public static var _lastDeviceLinkUpdate: [String:Date] = [:]
|
||||
/// A mapping from hex encoded public key to date updated.
|
||||
public static var lastDeviceLinkUpdate: [String:Date] {
|
||||
get { stateQueue.sync { _lastDeviceLinkUpdate } }
|
||||
set { stateQueue.sync { _lastDeviceLinkUpdate = newValue } }
|
||||
}
|
||||
|
||||
private static var _userHexEncodedPublicKeyCache: [String:Set<String>] = [:]
|
||||
/// A mapping from thread ID to set of user hex encoded public keys.
|
||||
@objc public static var userHexEncodedPublicKeyCache: [String:Set<String>] {
|
||||
get { stateQueue.sync { _userHexEncodedPublicKeyCache } }
|
||||
set { stateQueue.sync { _userHexEncodedPublicKeyCache = newValue } }
|
||||
}
|
||||
|
||||
private static let stateQueue = DispatchQueue(label: "stateQueue")
|
||||
|
||||
/// 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: "errorHandlingQueue")
|
||||
|
||||
// MARK: Convenience
|
||||
|
@ -12,12 +29,10 @@ public final class LokiAPI : NSObject {
|
|||
internal static let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
|
||||
// MARK: Settings
|
||||
private static let version = "v1"
|
||||
private static let apiVersion = "v1"
|
||||
private static let maxRetryCount: UInt = 8
|
||||
private static let defaultTimeout: TimeInterval = 20
|
||||
private static let longPollingTimeout: TimeInterval = 40
|
||||
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
|
||||
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
|
||||
private static var userIDScanLimit: UInt = 4096
|
||||
internal static var powDifficulty: UInt = 4
|
||||
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
|
||||
|
@ -69,6 +84,7 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
|
||||
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
|
||||
|
||||
public typealias RawResponsePromise = Promise<RawResponse>
|
||||
|
||||
// MARK: Lifecycle
|
||||
|
@ -77,7 +93,7 @@ public final class LokiAPI : NSObject {
|
|||
// MARK: Internal API
|
||||
internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String,
|
||||
parameters: [String:Any], headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
|
||||
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/\(version)")!
|
||||
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/\(apiVersion)")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ])
|
||||
if let headers = headers { request.allHTTPHeaderFields = headers }
|
||||
request.timeoutInterval = timeout ?? defaultTimeout
|
||||
|
@ -113,6 +129,7 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
|
||||
public static func getDestinations(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> Promise<[Destination]> {
|
||||
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
|
||||
let (promise, seal) = Promise<[Destination]>.pending()
|
||||
func getDestinations(in transaction: YapDatabaseReadTransaction? = nil) {
|
||||
func getDestinationsInternal(in transaction: YapDatabaseReadTransaction) {
|
||||
|
@ -169,11 +186,11 @@ public final class LokiAPI : NSObject {
|
|||
}
|
||||
func sendLokiMessageUsingSwarmAPI() -> Promise<Set<RawResponsePromise>> {
|
||||
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
|
||||
return lokiMessage.calculatePoW().then(on: DispatchQueue.global()) { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
|
||||
notificationCenter.post(name: .contactingNetwork, object: NSNumber(value: signalMessage.timestamp))
|
||||
return lokiMessage.calculatePoW().then { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
|
||||
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
|
||||
return getTargetSnodes(for: destination).map { swarm in
|
||||
return Set(swarm.map { target in
|
||||
notificationCenter.post(name: .sendingMessage, object: NSNumber(value: signalMessage.timestamp))
|
||||
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
|
||||
return sendLokiMessage(lokiMessageWithPoW, to: target).map { rawResponse in
|
||||
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||
guard powDifficulty != LokiAPI.powDifficulty else { return rawResponse }
|
||||
|
@ -193,7 +210,7 @@ public final class LokiAPI : NSObject {
|
|||
return Promise.value([ target ]).mapValues { sendLokiMessage(lokiMessage, to: $0) }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount).get { _ in
|
||||
LokiP2PAPI.markOnline(destination)
|
||||
onP2PSuccess()
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<Set<RawResponsePromise>> in
|
||||
}.recover { error -> Promise<Set<RawResponsePromise>> in
|
||||
LokiP2PAPI.markOffline(destination)
|
||||
if lokiMessage.isPing {
|
||||
print("[Loki] Failed to ping \(destination); marking contact as offline.")
|
||||
|
@ -310,7 +327,10 @@ public final class LokiAPI : NSObject {
|
|||
storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expirationDate, transaction: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static let receivedMessageHashValuesKey = "receivedMessageHashValuesKey"
|
||||
private static let receivedMessageHashValuesCollection = "receivedMessageHashValuesCollection"
|
||||
|
||||
private static func getReceivedMessageHashValues() -> Set<String>? {
|
||||
var result: Set<String>? = nil
|
||||
storage.dbReadConnection.read { transaction in
|
||||
|
@ -398,7 +418,7 @@ public final class LokiAPI : NSObject {
|
|||
private extension Promise {
|
||||
|
||||
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
|
||||
return recover(on: DispatchQueue.global()) { error -> Promise<T> in
|
||||
return recover { error -> Promise<T> in
|
||||
switch error {
|
||||
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
|
||||
case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error
|
||||
|
|
|
@ -159,11 +159,13 @@ public class LokiDotNetAPI : NSObject {
|
|||
}
|
||||
}
|
||||
if server == LokiFileServerAPI.server {
|
||||
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
DispatchQueue.global().async {
|
||||
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
}
|
||||
} else {
|
||||
getAuthToken(for: server).done(on: DispatchQueue.global()) { token in
|
||||
proceed(with: token)
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
}.catch { error in
|
||||
print("[Loki] Couldn't upload attachment due to error: \(error).")
|
||||
seal.reject(error)
|
||||
}
|
||||
|
@ -177,7 +179,8 @@ public class LokiDotNetAPI : NSObject {
|
|||
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: DispatchQueue.global()).map { rawResponse in
|
||||
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { 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
|
||||
|
@ -201,6 +204,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
|
||||
let parameters = [ "pubKey" : userHexEncodedPublicKey, "token" : token ]
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in token }
|
||||
}
|
||||
|
||||
|
|
|
@ -26,13 +26,14 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
/// Gets the device links associated with the given hex encoded public keys from the
|
||||
/// server and stores and returns the valid ones.
|
||||
public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>, in transaction: YapDatabaseReadWriteTransaction? = nil) -> Promise<Set<DeviceLink>> {
|
||||
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
|
||||
let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]"
|
||||
print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).")
|
||||
return getAuthToken(for: server, in: transaction).then(on: DispatchQueue.global()) { token -> Promise<Set<DeviceLink>> in
|
||||
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { $0.responseObject }.map(on: DispatchQueue.global()) { rawResponse -> Set<DeviceLink> in
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { rawResponse -> Set<DeviceLink> in
|
||||
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -85,7 +86,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
|
||||
public static func setDeviceLinks(_ deviceLinks: Set<DeviceLink>) -> Promise<Void> {
|
||||
print("[Loki] Updating device links.")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let isMaster = deviceLinks.contains { $0.master.hexEncodedPublicKey == userHexEncodedPublicKey }
|
||||
let deviceLinksAsJSON = deviceLinks.map { $0.toJSON() }
|
||||
let value = !deviceLinksAsJSON.isEmpty ? [ "isPrimary" : isMaster ? 1 : 0, "authorisations" : deviceLinksAsJSON ] : nil
|
||||
|
@ -94,7 +95,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover(on: DispatchQueue.global()) { error in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { _ in }.retryingIfNeeded(maxRetryCount: 8).recover { error in
|
||||
print("Couldn't update device links due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
|
@ -136,42 +137,33 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
// MARK: Profile Pictures (Public API)
|
||||
public static func setProfilePicture(_ profilePicture: Data) -> Promise<String> {
|
||||
return Promise<String>() { seal in
|
||||
guard profilePicture.count < maxFileSize else {
|
||||
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
|
||||
}
|
||||
getAuthToken(for: server).done { token in
|
||||
let url = "\(server)/users/me/avatar"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
formData.appendPart(withFileData: profilePicture, name: "avatar", fileName: UUID().uuidString, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
print("[Loki] Couldn't upload profile picture due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
let _ = LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).done { responseObject in
|
||||
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let profilePicture = data["avatar_image"] as? JSON, let downloadURL = profilePicture["url"] as? String else {
|
||||
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
|
||||
return seal.reject(LokiDotNetAPIError.parsingFailed)
|
||||
}
|
||||
return seal.fulfill(downloadURL)
|
||||
}.catch { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}.catch { error in
|
||||
print("[Loki] Couldn't upload profile picture due to error: \(error).")
|
||||
seal.reject(error)
|
||||
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
|
||||
guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) }
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
var request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
print("[Loki] Couldn't upload profile picture due to error: \(error).")
|
||||
return Promise(error: error)
|
||||
}
|
||||
return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map { responseObject in
|
||||
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
|
||||
print("[Loki] Couldn't parse profile picture from: \(responseObject).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
}
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = Date()
|
||||
return downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Profile Pictures (Public Obj-C API)
|
||||
@objc(setProfilePicture:)
|
||||
public static func objc_setProfilePicture(_ profilePicture: Data) -> AnyPromise {
|
||||
return AnyPromise.from(setProfilePicture(profilePicture))
|
||||
@objc(uploadProfilePicture:)
|
||||
public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
|
||||
return AnyPromise.from(uploadProfilePicture(profilePicture))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,79 +44,85 @@ internal class LokiFileServerProxy : LokiHTTPClient {
|
|||
}
|
||||
|
||||
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
|
||||
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
|
||||
guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) }
|
||||
var headers = getCanonicalHeaders(for: request)
|
||||
return LokiAPI.getRandomSnode().then { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise<Any> in
|
||||
let url = "\(proxy.address):\(proxy.port)/file_proxy"
|
||||
guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound,
|
||||
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
|
||||
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
|
||||
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
|
||||
print("[Loki] Proxying file server request (\(endpoint)) through \(proxy).")
|
||||
let parametersAsString: String
|
||||
if let tsRequest = request as? TSRequest {
|
||||
headers["Content-Type"] = "application/json"
|
||||
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [])
|
||||
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
|
||||
} else {
|
||||
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
||||
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
|
||||
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
||||
} else {
|
||||
parametersAsString = "null"
|
||||
return Promise<LokiAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
|
||||
DispatchQueue.global().async {
|
||||
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
|
||||
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
|
||||
LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise<Any> in
|
||||
let url = "\(proxy.address):\(proxy.port)/file_proxy"
|
||||
guard let urlAsString = request.url?.absoluteString, let serverURLEndIndex = urlAsString.range(of: server)?.upperBound,
|
||||
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
|
||||
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
|
||||
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
|
||||
print("[Loki] Proxying file server request (\(endpoint)) through \(proxy).")
|
||||
let parametersAsString: String
|
||||
if let tsRequest = request as? TSRequest {
|
||||
headers["Content-Type"] = "application/json"
|
||||
let parametersAsData = try JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [])
|
||||
parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null"
|
||||
} else {
|
||||
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
||||
if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) {
|
||||
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
||||
} else {
|
||||
parametersAsString = "null"
|
||||
}
|
||||
}
|
||||
let proxyRequestParameters: JSON = [
|
||||
"body" : parametersAsString,
|
||||
"endpoint": endpoint,
|
||||
"method" : request.httpMethod,
|
||||
"headers" : headers
|
||||
]
|
||||
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
|
||||
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
|
||||
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
|
||||
let proxyRequestHeaders = [
|
||||
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
|
||||
"X-Loki-File-Server-Verb" : "POST",
|
||||
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
|
||||
"Connection" : "close", // TODO: Is this necessary?
|
||||
"Content-Type" : "application/json"
|
||||
]
|
||||
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
|
||||
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
|
||||
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
|
||||
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
|
||||
proxyRequest.timeoutInterval = request.timeoutInterval
|
||||
var task: URLSessionDataTask!
|
||||
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
|
||||
if let error = error {
|
||||
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
|
||||
let nsError: NSError = nmError as NSError
|
||||
nsError.isRetryable = false
|
||||
resolver.reject(nsError)
|
||||
} else {
|
||||
resolver.fulfill(result)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return promise
|
||||
}.map(on: DispatchQueue.global()) { rawResponse in
|
||||
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: .allowFragments) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
|
||||
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
|
||||
print("[Loki] Received an invalid response.")
|
||||
throw Error.proxyResponseParsingFailed
|
||||
}
|
||||
let isSuccess = (200...299) ~= statusCode
|
||||
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: nil, underlyingError: Error.fileServerHTTPError(code: statusCode, message: nil)) }
|
||||
let uncheckedJSONAsData = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
|
||||
if uncheckedJSONAsData.isEmpty { return () }
|
||||
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: .allowFragments) as? JSON
|
||||
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
|
||||
return json
|
||||
}.done { rawResponse in
|
||||
seal.fulfill(rawResponse)
|
||||
}.catch { error in
|
||||
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
|
||||
seal.reject(HTTPError.from(error: error) ?? error)
|
||||
}
|
||||
}
|
||||
let proxyRequestParameters: JSON = [
|
||||
"body" : parametersAsString,
|
||||
"endpoint": endpoint,
|
||||
"method" : request.httpMethod,
|
||||
"headers" : headers
|
||||
]
|
||||
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
|
||||
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
|
||||
let base64EncodedPublicKey = Data(hex: keyPair.hexEncodedPublicKey).base64EncodedString() // The file server expects an 05 prefixed public key
|
||||
let proxyRequestHeaders = [
|
||||
"X-Loki-File-Server-Target" : "/loki/v1/secure_rpc",
|
||||
"X-Loki-File-Server-Verb" : "POST",
|
||||
"X-Loki-File-Server-Headers" : "{ \"X-Loki-File-Server-Ephemeral-Key\" : \"\(base64EncodedPublicKey)\" }",
|
||||
"Connection" : "close", // TODO: Is this necessary?
|
||||
"Content-Type" : "application/json"
|
||||
]
|
||||
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
|
||||
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
|
||||
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
|
||||
proxyRequest.httpBody = "{ \"cipherText64\" : \"\(ivAndCipherText.base64EncodedString())\" }".data(using: String.Encoding.utf8)!
|
||||
proxyRequest.timeoutInterval = request.timeoutInterval
|
||||
var task: URLSessionDataTask!
|
||||
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
|
||||
if let error = error {
|
||||
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
|
||||
let nsError: NSError = nmError as NSError
|
||||
nsError.isRetryable = false
|
||||
resolver.reject(nsError)
|
||||
} else {
|
||||
resolver.fulfill(result)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return promise
|
||||
}.map { rawResponse in
|
||||
guard let responseAsData = rawResponse as? Data, let responseAsJSON = try? JSONSerialization.jsonObject(with: responseAsData, options: .allowFragments) as? JSON, let base64EncodedCipherText = responseAsJSON["data"] as? String,
|
||||
let meta = responseAsJSON["meta"] as? JSON, let statusCode = meta["code"] as? Int, let cipherText = Data(base64Encoded: base64EncodedCipherText) else {
|
||||
print("[Loki] Received an invalid response.")
|
||||
throw Error.proxyResponseParsingFailed
|
||||
}
|
||||
let isSuccess = (200...299) ~= statusCode
|
||||
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: nil, underlyingError: Error.fileServerHTTPError(code: statusCode, message: nil)) }
|
||||
let uncheckedJSONAsData = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
|
||||
if uncheckedJSONAsData.isEmpty { return () }
|
||||
let uncheckedJSON = try? JSONSerialization.jsonObject(with: uncheckedJSONAsData, options: .allowFragments) as? JSON
|
||||
guard let json = uncheckedJSON else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
|
||||
return json
|
||||
}.recover { error -> Promise<Any> in
|
||||
print("[Loki] File server proxy request failed with error: \(error.localizedDescription).")
|
||||
throw HTTPError.from(error: error) ?? error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ public final class LokiLongPoller : NSObject {
|
|||
// MARK: Private API
|
||||
private func openConnections() {
|
||||
guard !hasStopped else { return }
|
||||
LokiAPI.getSwarm(for: userHexEncodedPublicKey).then(on: DispatchQueue.global()) { [weak self] _ -> Guarantee<[Result<Void>]> in
|
||||
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
|
||||
|
@ -52,7 +52,7 @@ public final class LokiLongPoller : NSObject {
|
|||
}
|
||||
strongSelf.connections = Set(connections)
|
||||
return when(resolved: connections)
|
||||
}.ensure(on: DispatchQueue.global()) { [weak self] in
|
||||
}.ensure { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
Timer.scheduledTimer(withTimeInterval: strongSelf.retryInterval, repeats: false) { _ in
|
||||
guard let strongSelf = self else { return }
|
||||
|
|
|
@ -31,63 +31,69 @@ internal class LokiSnodeProxy : LokiHTTPClient {
|
|||
// MARK: Proxying
|
||||
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
|
||||
guard let targetHexEncodedPublicKeySet = target.publicKeySet else { return Promise(error: Error.targetPublicKeySetMissing) }
|
||||
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
|
||||
guard let symmetricKey = uncheckedSymmetricKey else { return Promise(error: Error.symmetricKeyGenerationFailed) }
|
||||
let headers = getCanonicalHeaders(for: request)
|
||||
return LokiAPI.getRandomSnode().then { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] proxy -> Promise<Any> in
|
||||
let url = "\(proxy.address):\(proxy.port)/proxy"
|
||||
print("[Loki] Proxying request to \(target) through \(proxy).")
|
||||
let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
|
||||
let proxyRequestParameters: JSON = [
|
||||
"method" : request.httpMethod,
|
||||
"body" : String(bytes: parametersAsData, encoding: .utf8),
|
||||
"headers" : headers
|
||||
]
|
||||
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
|
||||
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
|
||||
let proxyRequestHeaders = [
|
||||
"X-Sender-Public-Key" : keyPair.publicKey.toHexString(),
|
||||
"X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
|
||||
]
|
||||
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
|
||||
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
|
||||
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
|
||||
proxyRequest.httpBody = ivAndCipherText
|
||||
proxyRequest.timeoutInterval = request.timeoutInterval
|
||||
var task: URLSessionDataTask!
|
||||
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
|
||||
if let error = error {
|
||||
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
|
||||
let nsError: NSError = nmError as NSError
|
||||
nsError.isRetryable = false
|
||||
resolver.reject(nsError)
|
||||
} else {
|
||||
resolver.fulfill(result)
|
||||
return Promise<LokiAPI.RawResponse> { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] seal in
|
||||
DispatchQueue.global().async {
|
||||
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
|
||||
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
|
||||
LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise<LokiAPI.RawResponse> in
|
||||
let url = "\(proxy.address):\(proxy.port)/proxy"
|
||||
print("[Loki] Proxying request to \(target) through \(proxy).")
|
||||
let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
|
||||
let proxyRequestParameters: JSON = [
|
||||
"method" : request.httpMethod,
|
||||
"body" : String(bytes: parametersAsData, encoding: .utf8),
|
||||
"headers" : headers
|
||||
]
|
||||
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
|
||||
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
|
||||
let proxyRequestHeaders = [
|
||||
"X-Sender-Public-Key" : keyPair.publicKey.toHexString(),
|
||||
"X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
|
||||
]
|
||||
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
|
||||
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
|
||||
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
|
||||
proxyRequest.httpBody = ivAndCipherText
|
||||
proxyRequest.timeoutInterval = request.timeoutInterval
|
||||
var task: URLSessionDataTask!
|
||||
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
|
||||
if let error = error {
|
||||
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
|
||||
let nsError: NSError = nmError as NSError
|
||||
nsError.isRetryable = false
|
||||
resolver.reject(nsError)
|
||||
} else {
|
||||
resolver.fulfill(result)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return promise
|
||||
}.map(on: DispatchQueue.global()) { rawResponse in
|
||||
guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) else {
|
||||
print("[Loki] Received a non-string encoded response.")
|
||||
return rawResponse
|
||||
}
|
||||
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
|
||||
let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
|
||||
guard let json = uncheckedJSON, let statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
|
||||
let isSuccess = (200...299) ~= statusCode
|
||||
var body: Any? = nil
|
||||
if let bodyAsString = json["body"] as? String {
|
||||
body = bodyAsString
|
||||
if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? JSON {
|
||||
body = bodyAsJSON
|
||||
}
|
||||
}
|
||||
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) }
|
||||
return body
|
||||
}.done { rawResponse in
|
||||
seal.fulfill(rawResponse)
|
||||
}.catch { error in
|
||||
print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
|
||||
seal.reject(HTTPError.from(error: error) ?? error)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return promise
|
||||
}.map { rawResponse in
|
||||
guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) else {
|
||||
print("[Loki] Received a non-string encoded response.")
|
||||
return rawResponse
|
||||
}
|
||||
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
|
||||
let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
|
||||
guard let json = uncheckedJSON, let statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
|
||||
let isSuccess = (200..<300).contains(statusCode)
|
||||
var body: Any? = nil
|
||||
if let bodyAsString = json["body"] as? String {
|
||||
body = bodyAsString
|
||||
if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? JSON {
|
||||
body = bodyAsJSON
|
||||
}
|
||||
}
|
||||
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) }
|
||||
return body
|
||||
}.recover { error -> Promise<Any> in
|
||||
print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
|
||||
throw HTTPError.from(error: error) ?? error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ public final class DeviceLink : NSObject, NSCoding {
|
|||
@objc public let signature: Data?
|
||||
|
||||
@objc public var displayName: String {
|
||||
if let customDisplayName = UserDefaults.standard.string(forKey: "\(hexEncodedPublicKey)_display_name") {
|
||||
if let customDisplayName = UserDefaults.standard[.slaveDeviceName(hexEncodedPublicKey)] {
|
||||
return customDisplayName
|
||||
} else {
|
||||
return NSLocalizedString("Unnamed Device", comment: "")
|
||||
|
|
|
@ -82,7 +82,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global()) { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let rawMessages = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -155,33 +155,41 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
public static func sendMessage(_ message: LokiPublicChatMessage, to channel: UInt64, on server: String) -> Promise<LokiPublicChatMessage> {
|
||||
guard let signedMessage = message.sign(with: userKeyPair.privateKey) else { return Promise(error: LokiDotNetAPIError.signingFailed) }
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<LokiPublicChatMessage> in
|
||||
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let displayName = userDisplayName
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
|
||||
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
return Promise<LokiPublicChatMessage> { [privateKey = userKeyPair.privateKey] seal in
|
||||
DispatchQueue.global().async {
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) }
|
||||
getAuthToken(for: server).then { token -> Promise<LokiPublicChatMessage> in
|
||||
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let displayName = userDisplayName
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let json = rawResponse as? JSON, let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
|
||||
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
print("[Loki] Couldn't parse message for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
|
||||
}
|
||||
}.recover { error -> Promise<LokiPublicChatMessage> in
|
||||
if let error = error as? NetworkManagerError, error.statusCode == 401 {
|
||||
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
|
||||
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
|
||||
}
|
||||
throw error
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount).done { message in
|
||||
seal.fulfill(message)
|
||||
}.catch { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: userHexEncodedPublicKey, displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
|
||||
}
|
||||
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiPublicChatMessage> in
|
||||
if let error = error as? NetworkManagerError, error.statusCode == 401 {
|
||||
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
|
||||
storage.dbReadWriteConnection.removeObject(forKey: server, inCollection: authTokenCollection)
|
||||
}
|
||||
throw error
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
|
||||
}
|
||||
}
|
||||
|
||||
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
|
||||
|
@ -194,7 +202,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let deletions = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse deleted messages for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -212,14 +220,14 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, isSentByUser: Bool) -> Promise<Void> {
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let isModerationRequest = !isSentByUser
|
||||
print("[Loki] Deleting message with ID: \(messageID) for public chat channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
|
||||
let urlAsString = isSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
|
||||
let url = URL(string: urlAsString)!
|
||||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
|
||||
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
|
||||
print("[Loki] Deleted message with ID: \(messageID) on server: \(server).")
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
|
||||
}
|
||||
|
@ -228,7 +236,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
|
||||
let request = TSRequest(url: url)
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let moderators = json["moderators"] as? [String] else {
|
||||
print("[Loki] Couldn't parse moderators for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -244,34 +252,34 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
|
||||
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
|
||||
print("[Loki] Joined channel with ID: \(channel) on server: \(server).")
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
|
||||
}
|
||||
}
|
||||
|
||||
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
|
||||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).done(on: DispatchQueue.global()) { result -> Void in
|
||||
return LokiFileServerProxy(for: server).perform(request).done { result -> Void in
|
||||
print("[Loki] Left channel with ID: \(channel) on server: \(server).")
|
||||
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
|
||||
}
|
||||
}
|
||||
|
||||
public static func getUserCount(for channel: UInt64, on server: String) -> Promise<Int> {
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Int> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Int> in
|
||||
let queryParameters = "count=200"
|
||||
let url = URL(string: "\(server)/channels/\(channel)/subscribers?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let users = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse user count for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -291,11 +299,11 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
guard let hexEncodedPublicKeys = displayNameUpdatees[publicChatID] else { return Promise.value(()) }
|
||||
displayNameUpdatees[publicChatID] = []
|
||||
print("[Loki] Getting display names for: \(hexEncodedPublicKeys).")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else {
|
||||
print("[Loki] Couldn't parse display names for users: \(hexEncodedPublicKeys) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
|
@ -320,12 +328,12 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
|
||||
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
|
||||
print("[Loki] Updating display name on server: \(server).")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
|
||||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in
|
||||
print("Couldn't update display name due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
|
@ -334,7 +342,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
|
||||
public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise<Void> {
|
||||
print("[Loki] Updating profile picture on server: \(server).")
|
||||
return getAuthToken(for: server).then(on: DispatchQueue.global()) { token -> Promise<Void> in
|
||||
return getAuthToken(for: server).then { token -> Promise<Void> in
|
||||
var annotation: JSON = [ "type" : profilePictureType ]
|
||||
if let url = url {
|
||||
annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ]
|
||||
|
@ -343,7 +351,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { _ in }.recover(on: DispatchQueue.global()) { error in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { _ in }.recover { error in
|
||||
print("[Loki] Couldn't update profile picture due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
|
@ -353,7 +361,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
public static func getInfo(for channel: UInt64, on server: String) -> Promise<LokiPublicChatInfo> {
|
||||
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
|
||||
let request = TSRequest(url: url)
|
||||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global()).map { rawResponse in
|
||||
return LokiFileServerProxy(for: server).perform(request).map { rawResponse in
|
||||
guard let json = rawResponse as? JSON,
|
||||
let data = json["data"] as? JSON,
|
||||
let annotations = data["annotations"] as? [JSON],
|
|
@ -56,7 +56,7 @@ public final class LokiPublicChatManager : NSObject {
|
|||
return Promise(error: Error.chatCreationFailed)
|
||||
}
|
||||
}
|
||||
return LokiPublicChatAPI.getAuthToken(for: server).then(on: DispatchQueue.global()) { token in
|
||||
return LokiPublicChatAPI.getAuthToken(for: server).then { token in
|
||||
return LokiPublicChatAPI.getInfo(for: channel, on: server)
|
||||
}.map { channelInfo -> LokiPublicChat in
|
||||
guard let chat = self.addChat(server: server, channel: channel, name: channelInfo.displayName) else { throw Error.chatCreationFailed }
|
|
@ -206,7 +206,9 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
proceed()
|
||||
DispatchQueue.global().async {
|
||||
proceed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ public final class SignalMessage : NSObject {
|
|||
public var ttl: UInt64? { return objc_ttl != 0 ? objc_ttl : nil }
|
||||
|
||||
@objc public init(type: SSKProtoEnvelope.SSKProtoEnvelopeType, timestamp: UInt64, senderID: String, senderDeviceID: UInt32,
|
||||
content: String, recipientID: String, ttl: UInt64, isPing: Bool, isFriendRequest: Bool) {
|
||||
content: String, recipientID: String, ttl: UInt64, isPing: Bool, isFriendRequest: Bool) {
|
||||
self.type = type
|
||||
self.timestamp = timestamp
|
||||
self.senderID = senderID
|
||||
|
|
|
@ -200,7 +200,7 @@ public class LokiP2PAPI : NSObject {
|
|||
AssertIsOnMainThread()
|
||||
|
||||
guard let message = onlineBroadcastMessage(forThread: thread) else {
|
||||
print("[Loki] P2P address not set.")
|
||||
// print("[Loki] P2P address not set.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ public class LokiP2PAPI : NSObject {
|
|||
|
||||
private static func createLokiAddressMessage(for thread: TSThread, isPing: Bool) -> LokiAddressMessage? {
|
||||
guard let ourAddress = ourP2PAddress else {
|
||||
print("[Loki] P2P address not set.")
|
||||
// print("[Loki] P2P address not set.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
62
SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
Normal file
62
SignalServiceKit/src/Loki/Utilities/LKUserDefaults.swift
Normal file
|
@ -0,0 +1,62 @@
|
|||
import Foundation
|
||||
|
||||
public enum LKUserDefaults {
|
||||
|
||||
public enum Bool : Swift.String {
|
||||
case hasLaunchedOnce
|
||||
case hasSeenOpenGroupSuggestionSheet
|
||||
case hasViewedSeed
|
||||
/// Whether the device was unlinked as a slave device (used to notify the user on the landing screen).
|
||||
case wasUnlinked
|
||||
}
|
||||
|
||||
public enum Date : Swift.String {
|
||||
case lastProfilePictureUpload
|
||||
}
|
||||
|
||||
public enum Double : Swift.String {
|
||||
case lastDeviceTokenUpload = "lastDeviceTokenUploadTime"
|
||||
}
|
||||
|
||||
public enum String {
|
||||
case slaveDeviceName(Swift.String)
|
||||
case deviceToken
|
||||
/// `nil` if this is a master device or if the user hasn't linked a device.
|
||||
case masterHexEncodedPublicKey
|
||||
|
||||
public var key: Swift.String {
|
||||
switch self {
|
||||
case .slaveDeviceName(let hexEncodedPublicKey): return "\(hexEncodedPublicKey)_display_name"
|
||||
case .deviceToken: return "deviceToken"
|
||||
case .masterHexEncodedPublicKey: return "masterDeviceHexEncodedPublicKey"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension UserDefaults {
|
||||
|
||||
public subscript(bool: LKUserDefaults.Bool) -> Bool {
|
||||
get { return self.bool(forKey: bool.rawValue) }
|
||||
set { set(newValue, forKey: bool.rawValue) }
|
||||
}
|
||||
|
||||
public subscript(date: LKUserDefaults.Date) -> Date? {
|
||||
get { return self.object(forKey: date.rawValue) as? Date }
|
||||
set { set(newValue, forKey: date.rawValue) }
|
||||
}
|
||||
|
||||
public subscript(double: LKUserDefaults.Double) -> Double {
|
||||
get { return self.double(forKey: double.rawValue) }
|
||||
set { set(newValue, forKey: double.rawValue) }
|
||||
}
|
||||
|
||||
public subscript(string: LKUserDefaults.String) -> String? {
|
||||
get { return self.string(forKey: string.key) }
|
||||
set { set(newValue, forKey: string.key) }
|
||||
}
|
||||
|
||||
public var isMasterDevice: Bool {
|
||||
return (self[.masterHexEncodedPublicKey] == nil)
|
||||
}
|
||||
}
|
|
@ -1,36 +1,40 @@
|
|||
|
||||
public extension Notification.Name {
|
||||
|
||||
// State changes
|
||||
public static let contactOnlineStatusChanged = Notification.Name("contactOnlineStatusChanged")
|
||||
public static let newMessagesReceived = Notification.Name("newMessagesReceived")
|
||||
public static let threadFriendRequestStatusChanged = Notification.Name("threadFriendRequestStatusChanged")
|
||||
public static let messageFriendRequestStatusChanged = Notification.Name("messageFriendRequestStatusChanged")
|
||||
public static let threadDeleted = Notification.Name("threadDeleted")
|
||||
public static let dataNukeRequested = Notification.Name("dataNukeRequested")
|
||||
public static let threadSessionRestoreDevicesChanged = Notification.Name("threadSessionRestoreDevicesChanged")
|
||||
// Message statuses
|
||||
// Message status changes
|
||||
public static let calculatingPoW = Notification.Name("calculatingPoW")
|
||||
public static let contactingNetwork = Notification.Name("contactingNetwork")
|
||||
public static let sendingMessage = Notification.Name("sendingMessage")
|
||||
public static let routing = Notification.Name("routing")
|
||||
public static let messageSending = Notification.Name("messageSending")
|
||||
public static let messageSent = Notification.Name("messageSent")
|
||||
public static let messageFailed = Notification.Name("messageFailed")
|
||||
// Onboarding
|
||||
public static let seedViewed = Notification.Name("seedViewed")
|
||||
// Interaction
|
||||
public static let dataNukeRequested = Notification.Name("dataNukeRequested")
|
||||
}
|
||||
|
||||
@objc public extension NSNotification {
|
||||
|
||||
// State changes
|
||||
@objc public static let contactOnlineStatusChanged = Notification.Name.contactOnlineStatusChanged.rawValue as NSString
|
||||
@objc public static let newMessagesReceived = Notification.Name.newMessagesReceived.rawValue as NSString
|
||||
@objc public static let threadFriendRequestStatusChanged = Notification.Name.threadFriendRequestStatusChanged.rawValue as NSString
|
||||
@objc public static let messageFriendRequestStatusChanged = Notification.Name.messageFriendRequestStatusChanged.rawValue as NSString
|
||||
@objc public static let threadDeleted = Notification.Name.threadDeleted.rawValue as NSString
|
||||
@objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
|
||||
@objc public static let threadSessionRestoreDevicesChanged = Notification.Name.threadSessionRestoreDevicesChanged.rawValue as NSString
|
||||
// Message statuses
|
||||
@objc public static let calculatingPoW = Notification.Name.calculatingPoW.rawValue as NSString
|
||||
@objc public static let contactingNetwork = Notification.Name.contactingNetwork.rawValue as NSString
|
||||
@objc public static let sendingMessage = Notification.Name.sendingMessage.rawValue as NSString
|
||||
@objc public static let routing = Notification.Name.routing.rawValue as NSString
|
||||
@objc public static let messageSending = Notification.Name.messageSending.rawValue as NSString
|
||||
@objc public static let messageSent = Notification.Name.messageSent.rawValue as NSString
|
||||
@objc public static let messageFailed = Notification.Name.messageFailed.rawValue as NSString
|
||||
// Onboarding
|
||||
@objc public static let seedViewed = Notification.Name.seedViewed.rawValue as NSString
|
||||
// Interaction
|
||||
@objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
|
||||
}
|
|
@ -43,6 +43,8 @@ extern NSString *const kAttachmentDownloadAttachmentIDKey;
|
|||
success:(void (^)(NSArray<TSAttachmentStream *> *attachmentStreams))success
|
||||
failure:(void (^)(NSError *error))failure;
|
||||
|
||||
- (void)continueDownloadIfPossible;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -264,6 +264,8 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
|
|||
|
||||
- (void)startDownloadIfPossible
|
||||
{
|
||||
if (CurrentAppContext().wasWokenUpBySilentPushNotification) { return; }
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
OWSAttachmentDownloadJob *_Nullable job;
|
||||
|
||||
|
@ -342,6 +344,16 @@ typedef void (^AttachmentDownloadFailure)(NSError *error);
|
|||
|
||||
#pragma mark -
|
||||
|
||||
- (void)continueDownloadIfPossible
|
||||
{
|
||||
if (self.attachmentDownloadJobQueue.count > 0) {
|
||||
[LKLogger print:@"[Loki] Continuing unfinished attachment download tasks."];
|
||||
[self startDownloadIfPossible];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)retrieveAttachmentForJob:(OWSAttachmentDownloadJob *)job
|
||||
success:(void (^)(TSAttachmentStream *attachmentStream))successHandler
|
||||
failure:(void (^)(NSError *error))failureHandler
|
||||
|
|
|
@ -350,21 +350,21 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
|
|||
|
||||
if (attachmentDescription.length > 0 && bodyDescription.length > 0) {
|
||||
// Attachment with caption.
|
||||
if ([CurrentAppContext() isRTL]) {
|
||||
return [[bodyDescription stringByAppendingString:@": "] stringByAppendingString:attachmentDescription];
|
||||
} else {
|
||||
// if ([CurrentAppContext() isRTL]) {
|
||||
// return [[bodyDescription stringByAppendingString:@": "] stringByAppendingString:attachmentDescription];
|
||||
// } else {
|
||||
return [[attachmentDescription stringByAppendingString:@": "] stringByAppendingString:bodyDescription];
|
||||
}
|
||||
// }
|
||||
} else if (bodyDescription.length > 0) {
|
||||
return bodyDescription;
|
||||
} else if (attachmentDescription.length > 0) {
|
||||
return attachmentDescription;
|
||||
} else if (self.contactShare) {
|
||||
if (CurrentAppContext().isRTL) {
|
||||
return [self.contactShare.name.displayName stringByAppendingString:@" 👤"];
|
||||
} else {
|
||||
// if (CurrentAppContext().isRTL) {
|
||||
// return [self.contactShare.name.displayName stringByAppendingString:@" 👤"];
|
||||
// } else {
|
||||
return [@"👤 " stringByAppendingString:self.contactShare.name.displayName];
|
||||
}
|
||||
// }
|
||||
} else {
|
||||
// OWSFailDebug(@"message has neither body nor attachment.");
|
||||
// TODO: We should do better here.
|
||||
|
|
|
@ -1408,14 +1408,14 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
// The envelope source is set during UD decryption.
|
||||
|
||||
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source]) {
|
||||
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil) { // Handled in LokiPublicChatPoller for open group messages
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
[[LKAPI getDestinationsFor:envelope.source inTransaction:transaction].ensureOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^() {
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}).catchOn(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(NSError *error) {
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}) retainUntilComplete];
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC));
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 4 * NSEC_PER_SEC));
|
||||
}
|
||||
|
||||
if (groupId.length > 0) {
|
||||
|
|
|
@ -37,6 +37,9 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
|
|||
|
||||
@property (nonatomic, readonly) BOOL isMainApp;
|
||||
@property (nonatomic, readonly) BOOL isMainAppAndActive;
|
||||
/// Whether the app was woken up by a silent push notification. This is important for
|
||||
/// determining whether attachments should be downloaded or not.
|
||||
@property (nonatomic) BOOL wasWokenUpBySilentPushNotification;
|
||||
|
||||
// Whether the user is using a right-to-left language like Arabic.
|
||||
@property (nonatomic, readonly) BOOL isRTL;
|
||||
|
|
Loading…
Reference in a new issue