Merge branch 'dev' of https://github.com/loki-project/loki-messenger-ios into sync-closed-group

This commit is contained in:
Ryan ZHAO 2020-02-21 15:11:20 +11:00
commit 63aafe06a1
49 changed files with 520 additions and 338 deletions

2
Pods

@ -1 +1 @@
Subproject commit eeaffe766824af6c6b7e7351da6bddb1e9d99611
Subproject commit 2870e676deec6a7ddb931edb6f0284f1f5b36085

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -11,6 +11,8 @@ extern NSString *const AppDelegateStoryboardMain;
- (void)startLongPollerIfNeeded;
- (void)stopLongPollerIfNeeded;
- (void)setUpDefaultPublicChatsIfNeeded;
- (void)startOpenGroupPollersIfNeeded;
- (void)stopOpenGroupPollersIfNeeded;
- (void)createRSSFeedsIfNeeded;
- (void)startRSSFeedPollersIfNeeded;

View File

@ -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];

View File

@ -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:

View File

@ -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.")
})

View File

@ -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)

View File

@ -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) {

View File

@ -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

View File

@ -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)
}
})
}

View File

@ -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()

View File

@ -71,7 +71,7 @@ final class OpenGroupSuggestionSheet : Sheet {
}
override func close() {
UserDefaults.standard.set(true, forKey: "hasSeenOpenGroupSuggestionSheet")
UserDefaults.standard[.hasSeenOpenGroupSuggestionSheet] = true
super.close()
}
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)))

View File

@ -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];

View File

@ -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];

View File

@ -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:)

View File

@ -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) {

View File

@ -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()
}

View File

@ -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 }
}
}

View File

@ -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

View File

@ -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 }
}

View File

@ -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))
}
}

View File

@ -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
}
}
}

View File

@ -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 }

View File

@ -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
}
}
}

View File

@ -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: "")

View File

@ -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],

View File

@ -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 }

View File

@ -206,7 +206,9 @@ public final class LokiPublicChatPoller : NSObject {
}
}
} else {
proceed()
DispatchQueue.global().async {
proceed()
}
}
}
}

View File

@ -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

View File

@ -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
}

View 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)
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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) {

View File

@ -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;