Implement polling, encryption & decryption
This commit is contained in:
parent
31dd2673e1
commit
80dcca627a
|
@ -22,7 +22,7 @@ final class NotificationServiceExtension : UNNotificationServiceExtension {
|
|||
// Modify the notification content here...
|
||||
let base64EncodedData = notificationContent.userInfo["ENCRYPTED_DATA"] as! String
|
||||
let data = Data(base64Encoded: base64EncodedData)!
|
||||
let envelope = try? LokiMessageWrapper.unwrap(data: data)
|
||||
let envelope = try? MessageWrapper.unwrap(data: data)
|
||||
let envelopeData = try? envelope?.serializedData()
|
||||
let decrypter = SSKEnvironment.shared.messageDecrypter
|
||||
if (envelope != nil && envelopeData != nil) {
|
||||
|
|
28
Podfile.lock
28
Podfile.lock
|
@ -60,10 +60,10 @@ PODS:
|
|||
- SessionCoreKit/Tests (1.0.0):
|
||||
- CocoaLumberjack
|
||||
- GRKOpenSSLFramework
|
||||
- SessionCurve25519Kit (2.1.2):
|
||||
- SessionCurve25519Kit (2.1.3):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit/Tests (2.1.2):
|
||||
- SessionCurve25519Kit/Tests (2.1.3):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionHKDFKit (0.0.5):
|
||||
|
@ -72,7 +72,7 @@ PODS:
|
|||
- SessionHKDFKit/Tests (0.0.5):
|
||||
- CocoaLumberjack
|
||||
- SessionCoreKit
|
||||
- SessionMetadataKit (1.0.2):
|
||||
- SessionMetadataKit (1.0.3):
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- SessionAxolotlKit (~> 1.0.2)
|
||||
|
@ -80,7 +80,7 @@ PODS:
|
|||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionHKDFKit (~> 0.0.5)
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- SessionMetadataKit/Tests (1.0.2):
|
||||
- SessionMetadataKit/Tests (1.0.3):
|
||||
- CocoaLumberjack
|
||||
- CryptoSwift (~> 1.3)
|
||||
- SessionAxolotlKit (~> 1.0.2)
|
||||
|
@ -100,8 +100,8 @@ PODS:
|
|||
- SAMKeychain
|
||||
- SessionAxolotlKit (~> 1.0.2)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionMetadataKit (~> 1.0.2)
|
||||
- SessionCurve25519Kit (~> 2.1.3)
|
||||
- SessionMetadataKit (~> 1.0.3)
|
||||
- Starscream
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher
|
||||
|
@ -117,8 +117,8 @@ PODS:
|
|||
- SAMKeychain
|
||||
- SessionAxolotlKit (~> 1.0.2)
|
||||
- SessionCoreKit (~> 1.0.0)
|
||||
- SessionCurve25519Kit (~> 2.1.2)
|
||||
- SessionMetadataKit (~> 1.0.2)
|
||||
- SessionCurve25519Kit (~> 2.1.3)
|
||||
- SessionMetadataKit (~> 1.0.3)
|
||||
- Starscream
|
||||
- SwiftProtobuf (~> 1.5.0)
|
||||
- YapDatabase/SQLCipher
|
||||
|
@ -283,13 +283,13 @@ CHECKOUT OPTIONS:
|
|||
:commit: 0d66c90657b62cb66ecd2767c57408a951650f23
|
||||
:git: https://github.com/loki-project/session-ios-core-kit.git
|
||||
SessionCurve25519Kit:
|
||||
:commit: ed4ae4576003905267d1573e336a003364564793
|
||||
:commit: c3bc075d1e1c8339eebe2af184869de1a007d855
|
||||
:git: https://github.com/loki-project/session-ios-curve-25519-kit
|
||||
SessionHKDFKit:
|
||||
:commit: 0dcf8cf8a7995ef8663146f7063e6c1d7f5a3274
|
||||
:git: https://github.com/nielsandriesse/session-ios-hkdf-kit.git
|
||||
SessionMetadataKit:
|
||||
:commit: bc6a425ac96dd6482b4e10df1eee1349153b2eca
|
||||
:commit: e23212e8494157d7a4daabbd4842be59117d9420
|
||||
:git: https://github.com/loki-project/session-ios-metadata-kit
|
||||
Starscream:
|
||||
:commit: b09ea163c3cb305152c65b299cb024610f52e735
|
||||
|
@ -314,13 +314,13 @@ SPEC CHECKSUMS:
|
|||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SessionAxolotlKit: 88f72573df989042510d8a1737bd0c0057dda3da
|
||||
SessionCoreKit: 778a3f6e3da788b43497734166646025b6392e88
|
||||
SessionCurve25519Kit: cedc6c713f8182e6ee02bf9bd1b337552e00ebbb
|
||||
SessionCurve25519Kit: 9bb9afe199e4bc23578a4b15932ad2c57bd047b1
|
||||
SessionHKDFKit: b0f4e669411703ab925aba07491c5611564d1419
|
||||
SessionMetadataKit: 29d6b1c0f8dbe630ccddd73362b9eb3b1abc8a6d
|
||||
SessionServiceKit: c792d64fc86aaa0f7325beaa36e1a9211a887359
|
||||
SessionMetadataKit: 581eb0da986e5a1752d07bfa89cf54bbe1fe3bca
|
||||
SessionServiceKit: 344dff85e344fd3177d7b0b7aff4647a9d5e9efc
|
||||
SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072
|
||||
SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9
|
||||
Starscream: ef3ece99d765eeccb67de105bfa143f929026cf5
|
||||
Starscream: 8aaf1a7feb805c816d0e7d3190ef23856f6665b9
|
||||
SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2
|
||||
YapDatabase: b418a4baa6906e8028748938f9159807fd039af4
|
||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||
|
|
2
Pods
2
Pods
|
@ -1 +1 @@
|
|||
Subproject commit 3353c303e2731e9266f62bf9b72cb7a49eeb969b
|
||||
Subproject commit 8e09671add980ffbb06ba27e1bbb2aa558b5b4cf
|
|
@ -38,7 +38,7 @@ A Swift/Objective-C library for communicating with the Session messaging service
|
|||
|
||||
s.resources = ["SignalServiceKit/Resources/Certificates/*", "SignalServiceKit/src/Loki/Mnemonic/*.txt"]
|
||||
|
||||
s.dependency 'SessionCurve25519Kit', '~> 2.1.2'
|
||||
s.dependency 'SessionCurve25519Kit', '~> 2.1.3'
|
||||
s.dependency 'CocoaLumberjack'
|
||||
s.dependency 'CryptoSwift', '~> 1.3'
|
||||
s.dependency 'AFNetworking'
|
||||
|
@ -52,7 +52,7 @@ A Swift/Objective-C library for communicating with the Session messaging service
|
|||
s.dependency 'Reachability'
|
||||
s.dependency 'SwiftProtobuf', '~> 1.5.0'
|
||||
s.dependency 'SessionCoreKit', '~> 1.0.0'
|
||||
s.dependency 'SessionMetadataKit', '~> 1.0.2'
|
||||
s.dependency 'SessionMetadataKit', '~> 1.0.3'
|
||||
s.dependency 'PromiseKit', '~> 6.0'
|
||||
|
||||
s.test_spec 'Tests' do |test_spec|
|
||||
|
|
|
@ -2789,6 +2789,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
B8CCF63B239757C10091D419 /* Components */,
|
||||
C32B405424A961E1001117B5 /* Dependencies */,
|
||||
B8BFFF392355426100102A27 /* Shelved */,
|
||||
B8CCF63C239757DB0091D419 /* Utilities */,
|
||||
B8CCF63D2397580E0091D419 /* View Controllers */,
|
||||
|
@ -2909,7 +2910,6 @@
|
|||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
|
||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
|
||||
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
|
||||
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2950,6 +2950,14 @@
|
|||
path = "View Controllers";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C32B405424A961E1001117B5 /* Dependencies */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */,
|
||||
);
|
||||
path = Dependencies;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C35E8AA42485C83B00ACB629 /* CSV */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
|
@ -9,8 +9,10 @@ extern NSString *const AppDelegateStoryboardMain;
|
|||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
- (void)startPollerIfNeeded;
|
||||
- (void)stopPollerIfNeeded;
|
||||
- (void)stopPoller;
|
||||
- (void)startClosedGroupPollerIfNeeded;
|
||||
- (void)stopClosedGroupPoller;
|
||||
- (void)startOpenGroupPollersIfNeeded;
|
||||
- (void)stopOpenGroupPollersIfNeeded;
|
||||
- (void)stopOpenGroupPollers;
|
||||
|
||||
@end
|
||||
|
|
|
@ -65,9 +65,8 @@ static NSTimeInterval launchStartedAt;
|
|||
@property (nonatomic) BOOL didAppLaunchFail;
|
||||
|
||||
// Loki
|
||||
@property (nonatomic) LKPoller *lokiPoller;
|
||||
@property (nonatomic) LKRSSFeedPoller *lokiNewsFeedPoller;
|
||||
@property (nonatomic) LKRSSFeedPoller *lokiMessengerUpdatesFeedPoller;
|
||||
@property (nonatomic) LKPoller *poller;
|
||||
@property (nonatomic) LKClosedGroupPoller *closedGroupPoller;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -1379,53 +1378,41 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
#pragma mark - Loki
|
||||
|
||||
- (void)setUpPollerIfNeeded
|
||||
{
|
||||
if (self.lokiPoller != nil) { return; }
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
if (userHexEncodedPublicKey == nil) { return; }
|
||||
self.lokiPoller = [[LKPoller alloc] initOnMessagesReceived:^(NSArray<SSKProtoEnvelope *> *messages) {
|
||||
if (messages.count != 0) {
|
||||
[LKLogger print:@"[Loki] Received new messages."];
|
||||
}
|
||||
for (SSKProtoEnvelope *message in messages) {
|
||||
NSData *data = [message serializedDataAndReturnError:nil];
|
||||
if (data != nil) {
|
||||
[SSKEnvironment.shared.messageReceiver handleReceivedEnvelopeData:data];
|
||||
} else {
|
||||
[LKLogger print:@"[Loki] Failed to deserialize envelope."];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)startPollerIfNeeded
|
||||
{
|
||||
[self setUpPollerIfNeeded];
|
||||
[self.lokiPoller startIfNeeded];
|
||||
if (self.poller != nil) { return; }
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
if (userPublicKey == nil) { return; }
|
||||
self.poller = [[LKPoller alloc] init];
|
||||
[self.poller startIfNeeded];
|
||||
}
|
||||
|
||||
- (void)stopPollerIfNeeded
|
||||
- (void)stopPoller { [self.lokiPoller stop]; }
|
||||
|
||||
- (void)startClosedGroupPollerIfNeeded
|
||||
{
|
||||
[self.lokiPoller stopIfNeeded];
|
||||
if (self.closedGroupPoller != nil) { return; }
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
if (userPublicKey == nil) { return; }
|
||||
self.closedGroupPoller = [[LKClosedGroupPoller alloc] init];
|
||||
[self.closedGroupPoller startIfNeeded];
|
||||
}
|
||||
|
||||
- (void)stopClosedGroupPoller { [self.closedGroupPoller stop]; }
|
||||
|
||||
- (void)startOpenGroupPollersIfNeeded
|
||||
{
|
||||
[LKPublicChatManager.shared startPollersIfNeeded];
|
||||
[SSKEnvironment.shared.attachmentDownloads continueDownloadIfPossible];
|
||||
}
|
||||
|
||||
- (void)stopOpenGroupPollersIfNeeded
|
||||
{
|
||||
[LKPublicChatManager.shared stopPollers];
|
||||
}
|
||||
- (void)stopOpenGroupPollers { [LKPublicChatManager.shared stopPollers]; }
|
||||
|
||||
- (void)handleDataNukeRequested:(NSNotification *)notification {
|
||||
[ThreadUtil deleteAllContent];
|
||||
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
|
||||
[SSKEnvironment.shared.identityManager clearIdentityKey];
|
||||
[LKAPI clearSnodePool];
|
||||
[LKSnodeAPI clearSnodePool];
|
||||
[self stopPollerIfNeeded];
|
||||
[self stopOpenGroupPollersIfNeeded];
|
||||
[self.lokiNewsFeedPoller stop];
|
||||
|
|
|
@ -199,7 +199,7 @@ public class MessageFetcherJob: NSObject {
|
|||
}
|
||||
|
||||
private func fetchUndeliveredMessages() -> Promise<Set<Promise<[SSKProtoEnvelope]>>> {
|
||||
return LokiAPI.getMessages()
|
||||
return SnodeAPI.getMessages(for: getUserHexEncodedPublicKey())
|
||||
}
|
||||
|
||||
private func acknowledgeDelivery(envelope: SSKProtoEnvelope) {
|
||||
|
|
|
@ -3,6 +3,24 @@ class BaseVC : UIViewController {
|
|||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle { return isLightMode ? .default : .lightContent }
|
||||
|
||||
lazy var navBarTitleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.alpha = 1
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
lazy var crossfadeLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.alpha = 0
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
override func viewDidLoad() {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(handleUnexpectedDeviceLinkRequestReceivedNotification), name: .unexpectedDeviceLinkRequestReceived, object: nil)
|
||||
|
@ -23,11 +41,18 @@ class BaseVC : UIViewController {
|
|||
}
|
||||
|
||||
internal func setNavBarTitle(_ title: String, customFontSize: CGFloat? = nil) {
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.text = title
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: customFontSize ?? Values.veryLargeFontSize)
|
||||
navigationItem.titleView = titleLabel
|
||||
let container = UIView()
|
||||
navBarTitleLabel.text = title
|
||||
crossfadeLabel.text = title
|
||||
if let customFontSize = customFontSize {
|
||||
navBarTitleLabel.font = .boldSystemFont(ofSize: customFontSize)
|
||||
crossfadeLabel.font = .boldSystemFont(ofSize: customFontSize)
|
||||
}
|
||||
container.addSubview(navBarTitleLabel)
|
||||
navBarTitleLabel.pin(to: container)
|
||||
container.addSubview(crossfadeLabel)
|
||||
crossfadeLabel.pin(to: container)
|
||||
navigationItem.titleView = container
|
||||
}
|
||||
|
||||
internal func setUpNavBarSessionIcon() {
|
||||
|
|
|
@ -182,7 +182,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
let linkingAuthorizationMessage = DeviceLinkingUtilities.getLinkingAuthorizationMessage(for: deviceLink)
|
||||
let master = DeviceLink.Device(hexEncodedPublicKey: deviceLink.master.hexEncodedPublicKey, signature: linkingAuthorizationMessage.masterSignature)
|
||||
let signedDeviceLink = DeviceLink(between: master, and: deviceLink.slave)
|
||||
LokiFileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
|
||||
FileServerAPI.addDeviceLink(signedDeviceLink).done(on: DispatchQueue.main) { [weak self] in
|
||||
SSKEnvironment.shared.messageSender.send(linkingAuthorizationMessage, success: {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
let slaveHexEncodedPublicKey = deviceLink.slave.hexEncodedPublicKey
|
||||
|
@ -205,7 +205,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
}
|
||||
}, failure: { error in
|
||||
print("[Loki] Failed to send device link authorization message.")
|
||||
let _ = LokiFileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
|
||||
let _ = FileServerAPI.removeDeviceLink(signedDeviceLink) // Attempt to roll back
|
||||
DispatchQueue.main.async {
|
||||
self?.close()
|
||||
let alert = UIAlertController(title: NSLocalizedString("Device Linking Failed", comment: ""), message: NSLocalizedString("Please check your internet connection and try again", comment: ""), preferredStyle: .alert)
|
||||
|
@ -233,7 +233,7 @@ final class DeviceLinkingModal : Modal, DeviceLinkingSessionDelegate {
|
|||
subtitleLabel.text = NSLocalizedString("Your device has been linked successfully", comment: "")
|
||||
mnemonicLabel.isHidden = true
|
||||
buttonStackView.isHidden = true
|
||||
LokiFileServerAPI.addDeviceLink(deviceLink).catch { error in
|
||||
FileServerAPI.addDeviceLink(deviceLink).catch { error in
|
||||
print("[Loki] Failed to add device link due to error: \(error).")
|
||||
}
|
||||
Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in
|
||||
|
|
|
@ -140,7 +140,7 @@ final class DeviceLinksVC : BaseVC, UITableViewDataSource, UITableViewDelegate,
|
|||
}
|
||||
|
||||
private func removeDeviceLink(_ deviceLink: DeviceLink) {
|
||||
LokiFileServerAPI.removeDeviceLink(deviceLink).done { [weak self] in
|
||||
FileServerAPI.removeDeviceLink(deviceLink).done { [weak self] in
|
||||
let linkedDeviceHexEncodedPublicKey = deviceLink.other.hexEncodedPublicKey
|
||||
guard let thread = TSContactThread.fetch(uniqueId: TSContactThread.threadId(fromContactId: linkedDeviceHexEncodedPublicKey)) else { return }
|
||||
let unlinkDeviceMessage = UnlinkDeviceMessage(thread: thread)
|
||||
|
|
|
@ -45,6 +45,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
|
||||
let bottomInset = Values.newConversationButtonBottomOffset + Values.newConversationButtonExpandedSize + Values.largeSpacing + Values.newConversationButtonCollapsedSize
|
||||
result.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: bottomInset, right: 0)
|
||||
result.showsVerticalScrollIndicator = false
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -166,7 +167,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol
|
|||
publicKeys.insert(publicKey)
|
||||
}
|
||||
}
|
||||
let _ = LokiFileServerAPI.getDeviceLinks(associatedWith: publicKeys)
|
||||
let _ = FileServerAPI.getDeviceLinks(associatedWith: publicKeys)
|
||||
// Do initial update
|
||||
reload()
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate {
|
||||
final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, UIScrollViewDelegate {
|
||||
private var selectedContacts: Set<String> = []
|
||||
|
||||
private lazy var contacts: [String] = {
|
||||
|
@ -8,22 +8,22 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
storage.dbReadConnection.read { transaction in
|
||||
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||
guard let thread = object as? TSContactThread, thread.shouldThreadBeVisible && thread.isContactFriend else { return }
|
||||
let hexEncodedPublicKey = thread.contactIdentifier()
|
||||
guard UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) != nil else { return }
|
||||
let publicKey = thread.contactIdentifier()
|
||||
guard UserDisplayNameUtilities.getPrivateChatDisplayName(for: publicKey) != nil else { return }
|
||||
// We shouldn't be able to add slave devices to groups
|
||||
guard storage.getMasterHexEncodedPublicKey(for: hexEncodedPublicKey, in: transaction) == nil else { return }
|
||||
result.append(hexEncodedPublicKey)
|
||||
guard storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) == nil else { return }
|
||||
result.append(publicKey)
|
||||
}
|
||||
}
|
||||
func getDisplayName(for hexEncodedPublicKey: String) -> String {
|
||||
return UserDisplayNameUtilities.getPrivateChatDisplayName(for: hexEncodedPublicKey) ?? "Unknown Contact"
|
||||
}
|
||||
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
var linkedDeviceHexEncodedPublicKeys: Set<String> = [ userHexEncodedPublicKey ]
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
var userLinkedDevices: Set<String> = [ userPublicKey ]
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
linkedDeviceHexEncodedPublicKeys = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userHexEncodedPublicKey, in: transaction)
|
||||
userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
|
||||
}
|
||||
result = result.filter { !linkedDeviceHexEncodedPublicKeys.contains($0) }
|
||||
result = result.filter { !userLinkedDevices.contains($0) }
|
||||
result = result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
||||
return result
|
||||
}()
|
||||
|
@ -38,7 +38,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
result.register(Cell.self, forCellReuseIdentifier: "Cell")
|
||||
result.separatorStyle = .none
|
||||
result.backgroundColor = .clear
|
||||
result.showsVerticalScrollIndicator = false
|
||||
result.isScrollEnabled = false
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -47,7 +47,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
super.viewDidLoad()
|
||||
setUpGradientBackground()
|
||||
setUpNavBarStyle()
|
||||
let customTitleFontSize = isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize
|
||||
let customTitleFontSize = Values.largeFontSize
|
||||
setNavBarTitle(NSLocalizedString("New Closed Group", comment: ""), customFontSize: customTitleFontSize)
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
|
@ -58,33 +58,29 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
navigationItem.rightBarButtonItem = doneButton
|
||||
// Set up content
|
||||
if !contacts.isEmpty {
|
||||
view.addSubview(nameTextField)
|
||||
nameTextField.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
|
||||
nameTextField.pin(.top, to: .top, of: view, withInset: Values.mediumSpacing)
|
||||
nameTextField.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity)
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.text = NSLocalizedString("Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions.", comment: "")
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
view.addSubview(explanationLabel)
|
||||
explanationLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing)
|
||||
explanationLabel.pin(.top, to: .bottom, of: nameTextField, withInset: Values.mediumSpacing)
|
||||
explanationLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing)
|
||||
let mainStackView = UIStackView()
|
||||
mainStackView.axis = .vertical
|
||||
nameTextField.delegate = self
|
||||
let nameTextFieldContainer = UIView()
|
||||
nameTextFieldContainer.addSubview(nameTextField)
|
||||
nameTextField.pin(.leading, to: .leading, of: nameTextFieldContainer, withInset: Values.largeSpacing)
|
||||
nameTextField.pin(.top, to: .top, of: nameTextFieldContainer, withInset: Values.mediumSpacing)
|
||||
nameTextFieldContainer.pin(.trailing, to: .trailing, of: nameTextField, withInset: Values.largeSpacing)
|
||||
nameTextFieldContainer.pin(.bottom, to: .bottom, of: nameTextField, withInset: Values.largeSpacing)
|
||||
mainStackView.addArrangedSubview(nameTextFieldContainer)
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = Colors.separator
|
||||
separator.set(.height, to: Values.separatorThickness)
|
||||
view.addSubview(separator)
|
||||
separator.pin(.leading, to: .leading, of: view)
|
||||
separator.pin(.top, to: .bottom, of: explanationLabel, withInset: Values.largeSpacing)
|
||||
separator.pin(.trailing, to: .trailing, of: view)
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(.leading, to: .leading, of: view)
|
||||
tableView.pin(.top, to: .bottom, of: separator)
|
||||
tableView.pin(.trailing, to: .trailing, of: view)
|
||||
tableView.pin(.bottom, to: .bottom, of: view)
|
||||
mainStackView.addArrangedSubview(separator)
|
||||
tableView.set(.height, to: CGFloat(contacts.count * 67)) // A cell is exactly 67 points high
|
||||
tableView.set(.width, to: UIScreen.main.bounds.width)
|
||||
mainStackView.addArrangedSubview(tableView)
|
||||
let scrollView = UIScrollView(wrapping: mainStackView, withInsets: UIEdgeInsets.zero)
|
||||
scrollView.showsVerticalScrollIndicator = false
|
||||
scrollView.delegate = self
|
||||
view.addSubview(scrollView)
|
||||
scrollView.set(.width, to: UIScreen.main.bounds.width)
|
||||
scrollView.pin(to: view)
|
||||
} else {
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
|
@ -122,6 +118,22 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
// MARK: Interaction
|
||||
func textFieldDidEndEditing(_ textField: UITextField) {
|
||||
crossfadeLabel.text = textField.text!.isEmpty ? NSLocalizedString("New Closed Group", comment: "") : textField.text!
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if nameTextField.isFirstResponder {
|
||||
nameTextField.resignFirstResponder()
|
||||
}
|
||||
let nameTextFieldCenterY = nameTextField.convert(nameTextField.bounds.center, to: scrollView).y
|
||||
let tableViewOriginY = tableView.convert(tableView.bounds.origin, to: scrollView).y
|
||||
let titleLabelAlpha = 1 - (scrollView.contentOffset.y - nameTextFieldCenterY) / (tableViewOriginY - nameTextFieldCenterY)
|
||||
let crossfadeLabelAlpha = 1 - titleLabelAlpha
|
||||
navBarTitleLabel.alpha = titleLabelAlpha
|
||||
crossfadeLabel.alpha = crossfadeLabelAlpha
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let contact = contacts[indexPath.row]
|
||||
if !selectedContacts.contains(contact) {
|
||||
|
@ -153,38 +165,20 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
guard selectedContacts.count >= 2 else {
|
||||
return showError(title: NSLocalizedString("Please pick at least 2 group members", comment: ""))
|
||||
}
|
||||
guard selectedContacts.count <= 10 else {
|
||||
return showError(title: NSLocalizedString("A closed group cannot have more than 10 members", comment: ""))
|
||||
guard selectedContacts.count <= 20 else {
|
||||
return showError(title: NSLocalizedString("A closed group cannot have more than 20 members", comment: ""))
|
||||
}
|
||||
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
var masterHexEncodedPublicKey = ""
|
||||
storage.dbReadConnection.read { transaction in
|
||||
masterHexEncodedPublicKey = storage.getMasterHexEncodedPublicKey(for: userHexEncodedPublicKey, in: transaction) ?? userHexEncodedPublicKey
|
||||
}
|
||||
let members = selectedContacts + [ masterHexEncodedPublicKey ]
|
||||
let admins = [ masterHexEncodedPublicKey ]
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(Randomness.generateRandomBytes(kGroupIdLength)!.toHexString())
|
||||
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group)
|
||||
OWSProfileManager.shared().addThread(toProfileWhitelist: thread)
|
||||
let selectedContacts = self.selectedContacts
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] modalActivityIndicator in
|
||||
let message = TSOutgoingMessage(in: thread, groupMetaMessage: .new, expiresInSeconds: 0)
|
||||
message.update(withCustomMessage: "Closed group created")
|
||||
DispatchQueue.main.async {
|
||||
SSKEnvironment.shared.messageSender.send(message, success: {
|
||||
DispatchQueue.main.async {
|
||||
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 {
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
})
|
||||
let _ = FileServerAPI.getDeviceLinks(associatedWith: selectedContacts).ensure2 {
|
||||
var thread: TSGroupThread!
|
||||
try! Storage.writeSync { transaction in
|
||||
thread = ClosedGroupsProtocol.createClosedGroup(name: name, members: selectedContacts, transaction: transaction)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self?.presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
SignalApp.shared().presentConversation(for: thread, action: .compose, animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ final class PathVC : BaseVC {
|
|||
return stackView
|
||||
}
|
||||
|
||||
private func getPathRow(snode: LokiAPITarget, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
|
||||
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
|
||||
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
|
||||
let title = isGuardSnode ? NSLocalizedString("Entry Node", comment: "") : NSLocalizedString("Service Node", comment: "")
|
||||
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
|
||||
|
|
|
@ -263,7 +263,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
var isMaxFileSizeExceeded = false
|
||||
if let error = error as? LokiDotNetAPI.LokiDotNetAPIError {
|
||||
if let error = error as? DotNetAPI.DotNetAPIError {
|
||||
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
|
||||
}
|
||||
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : NSLocalizedString("Couldn't Update Profile", comment: "")
|
||||
|
|
|
@ -679,7 +679,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
|
|||
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
|
||||
[ThreadUtil deleteAllContent];
|
||||
[SSKEnvironment.shared.identityManager clearIdentityKey];
|
||||
[LKAPI clearSnodePool];
|
||||
[LKSnodeAPI clearSnodePool];
|
||||
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
|
||||
[appDelegate stopPollerIfNeeded];
|
||||
[appDelegate stopOpenGroupPollersIfNeeded];
|
||||
|
|
|
@ -2806,8 +2806,8 @@
|
|||
"Search GIFs?" = "Search GIFs?";
|
||||
"You will not have full metadata protection when sending GIFs." = "You will not have full metadata protection when sending GIFs.";
|
||||
"The ability to add members to a closed group is coming soon." = "The ability to add members to a closed group is coming soon.";
|
||||
"A closed group cannot have more than 10 members" = "A closed group cannot have more than 10 members";
|
||||
"Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions." = "Closed groups support up to 10 members and provide the same privacy protections as one-on-one sessions.";
|
||||
"A closed group cannot have more than 20 members" = "A closed group cannot have more than 20 members";
|
||||
"Closed groups support up to 20 members" = "Closed groups support up to 20 members";
|
||||
"No messages yet" = "No messages yet";
|
||||
"Would you like to join the Session Public Chat?" = "Would you like to join the Session Public Chat?";
|
||||
"Join Public Chat" = "Join Public Chat";
|
||||
|
@ -2842,3 +2842,4 @@
|
|||
"Learn More" = "Learn More";
|
||||
"Please ask the open group operator to add you to the group." = "Please ask the open group operator to add you to the group.";
|
||||
"Unauthorized" = "Unauthorized";
|
||||
"Closed group created" = "Closed group created";
|
||||
|
|
|
@ -229,7 +229,6 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
|
|||
return;
|
||||
}
|
||||
|
||||
if ([LKSyncMessagesProtocol shouldSkipConfigurationSyncMessage]) { return; }
|
||||
[self sendConfigurationSyncMessage_AppReady];
|
||||
}];
|
||||
}
|
||||
|
@ -270,7 +269,7 @@ NSString *const kSyncManagerLastContactSyncKey = @"kTSStorageManagerOWSSyncManag
|
|||
|
||||
- (AnyPromise *)syncContact:(NSString *)hexEncodedPubKey transaction:(YapDatabaseReadTransaction *)transaction
|
||||
{
|
||||
return [LKSyncMessagesProtocol syncContactWithPublicKey:hexEncodedPubKey in:transaction];
|
||||
return [LKSyncMessagesProtocol syncContactWithPublicKey:hexEncodedPubKey];
|
||||
}
|
||||
|
||||
- (AnyPromise *)syncAllContacts
|
||||
|
|
|
@ -16,13 +16,14 @@ option java_outer_classname = "SignalServiceProtos";
|
|||
|
||||
message Envelope {
|
||||
enum Type {
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
FRIEND_REQUEST = 101; // Loki: Contains prekeys and a message; uses simple encryption
|
||||
UNKNOWN = 0;
|
||||
CIPHERTEXT = 1;
|
||||
KEY_EXCHANGE = 2;
|
||||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
CLOSED_GROUP_CIPHERTEXT = 7; // Loki
|
||||
FRIEND_REQUEST = 101; // Loki: Contains pre keys and a message; uses simple encryption
|
||||
}
|
||||
|
||||
// @required
|
||||
|
@ -131,6 +132,15 @@ message CallMessage {
|
|||
optional bytes profileKey = 6;
|
||||
}
|
||||
|
||||
message ClosedGroupCiphertext {
|
||||
// @required
|
||||
optional bytes ciphertext = 1;
|
||||
// @required
|
||||
optional string senderPublicKey = 2;
|
||||
// @required
|
||||
optional uint32 keyIndex = 3;
|
||||
}
|
||||
|
||||
message DataMessage {
|
||||
enum Flags {
|
||||
END_SESSION = 1;
|
||||
|
@ -235,22 +245,25 @@ message DataMessage {
|
|||
optional AttachmentPointer image = 3;
|
||||
}
|
||||
|
||||
// Loki: A custom message for our profile
|
||||
message LokiProfile {
|
||||
message LokiProfile { // Loki
|
||||
optional string displayName = 1;
|
||||
optional string profilePicture = 2;
|
||||
}
|
||||
|
||||
message ClosedGroupUpdate { // Loki
|
||||
enum Type {
|
||||
NEW = 0; // groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
|
||||
}
|
||||
|
||||
optional string name = 1;
|
||||
// @required
|
||||
optional string name = 1;
|
||||
optional bytes groupPublicKey = 2;
|
||||
optional bytes groupPrivateKey = 3;
|
||||
repeated bytes chainKeys = 4;
|
||||
repeated string members = 5;
|
||||
repeated string admins = 6;
|
||||
// @required
|
||||
optional string groupID = 2;
|
||||
// @required
|
||||
optional string sharedSecret = 3;
|
||||
// @required
|
||||
optional string senderKey = 4;
|
||||
repeated string members = 5;
|
||||
optional Type type = 7;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
|
@ -327,7 +340,7 @@ message SyncMessage {
|
|||
// @required
|
||||
optional string url = 1;
|
||||
// @required
|
||||
optional uint64 channelId = 2;
|
||||
optional uint64 channelID = 2;
|
||||
}
|
||||
|
||||
message Blocked {
|
||||
|
@ -445,7 +458,7 @@ message GroupDetails {
|
|||
optional uint32 expireTimer = 6;
|
||||
optional string color = 7;
|
||||
optional bool blocked = 8;
|
||||
repeated string admins = 9; // Loki
|
||||
repeated string admins = 9; // Loki
|
||||
}
|
||||
|
||||
// Internal - DO NOT SEND
|
||||
|
|
|
@ -19,6 +19,7 @@ extern NSString *const TSGroupThread_NotificationKey_UniqueId;
|
|||
@property (nonatomic, strong) TSGroupModel *groupModel;
|
||||
@property (nonatomic, readonly) BOOL isRSSFeed;
|
||||
@property (nonatomic, readonly) BOOL isPublicChat;
|
||||
@property (nonatomic) BOOL usesSharedSenderKeys;
|
||||
|
||||
+ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel;
|
||||
+ (instancetype)getOrCreateThreadWithGroupModel:(TSGroupModel *)groupModel
|
||||
|
|
|
@ -25,12 +25,14 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
OWSAssertDebug(groupModel);
|
||||
OWSAssertDebug(groupModel.groupId.length > 0);
|
||||
OWSAssertDebug(groupModel.groupMemberIds.count > 0);
|
||||
|
||||
for (NSString *recipientId in groupModel.groupMemberIds) {
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
}
|
||||
|
||||
NSString *uniqueIdentifier = [[self class] threadIdFromGroupId:groupModel.groupId];
|
||||
self = [super initWithUniqueId:uniqueIdentifier];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
@ -55,6 +57,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
adminIds:@[ localNumber ]];
|
||||
|
||||
self = [self initWithGroupModel:groupModel];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
@ -77,10 +80,12 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
OWSAssertDebug(transaction);
|
||||
|
||||
TSGroupThread *thread = [self fetchObjectWithUniqueID:[self threadIdFromGroupId:groupId] transaction:transaction];
|
||||
|
||||
if (!thread) {
|
||||
thread = [[self alloc] initWithGroupId:groupId groupType:groupType];
|
||||
[thread saveWithTransaction:transaction];
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
|
@ -89,9 +94,11 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
OWSAssertDebug(groupId.length > 0);
|
||||
|
||||
__block TSGroupThread *thread;
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
thread = [self getOrCreateThreadWithGroupId:groupId groupType:groupType transaction:transaction];
|
||||
} error:nil];
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
|
@ -108,6 +115,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
thread = [[TSGroupThread alloc] initWithGroupModel:groupModel];
|
||||
[thread saveWithTransaction:transaction];
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
|
@ -117,15 +125,18 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
OWSAssertDebug(groupModel.groupId.length > 0);
|
||||
|
||||
__block TSGroupThread *thread;
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
thread = [self getOrCreateThreadWithGroupModel:groupModel transaction:transaction];
|
||||
} error:nil];
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
+ (NSString *)threadIdFromGroupId:(NSData *)groupId
|
||||
{
|
||||
OWSAssertDebug(groupId.length > 0);
|
||||
|
||||
return [TSGroupThreadPrefix stringByAppendingString:[[LKGroupUtilities getDecodedGroupIDAsData:groupId] base64EncodedString]];
|
||||
}
|
||||
|
||||
|
@ -188,12 +199,19 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
return (self.groupModel.groupType == rssFeed);
|
||||
}
|
||||
|
||||
- (BOOL)isContactFriend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
- (BOOL)isLocalUserInGroup
|
||||
{
|
||||
__block BOOL result = NO;
|
||||
|
||||
[OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
result = [self isCurrentUserInGroupWithTransaction:transaction];
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -234,6 +252,7 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
- (void)setGroupModel:(TSGroupModel *)newGroupModel withTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
self.groupModel = newGroupModel;
|
||||
|
||||
[self saveWithTransaction:transaction];
|
||||
|
||||
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
|
||||
|
@ -251,10 +270,10 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
- (void)leaveGroupWithTransaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
NSMutableSet<NSString *> *newGroupMemberIDs = [NSMutableSet setWithArray:self.groupModel.groupMemberIds];
|
||||
NSString *userHexEncodedPublicKey = TSAccountManager.localNumber;
|
||||
if (userHexEncodedPublicKey == nil) { return; }
|
||||
NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userHexEncodedPublicKey in:transaction];
|
||||
[newGroupMemberIDs minusSet:linkedDeviceHexEncodedPublicKeys];
|
||||
NSString *userPublicKey = TSAccountManager.localNumber;
|
||||
if (userPublicKey == nil) { return; }
|
||||
NSSet<NSString *> *userLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:userPublicKey in:transaction];
|
||||
[newGroupMemberIDs minusSet:userLinkedDevices];
|
||||
self.groupModel.groupMemberIds = newGroupMemberIDs.allObjects;
|
||||
[self saveWithTransaction:transaction];
|
||||
[transaction addCompletionQueue:dispatch_get_main_queue() completionBlock:^{
|
||||
|
@ -315,11 +334,6 @@ NSString *const TSGroupThread_NotificationKey_UniqueId = @"TSGroupThread_Notific
|
|||
return [self.class stableColorNameForNewConversationWithString:[self threadIdFromGroupId:groupId]];
|
||||
}
|
||||
|
||||
- (BOOL)isContactFriend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -60,7 +60,7 @@ public enum ProofOfWork {
|
|||
/// - Returns: A nonce string or `nil` if it failed
|
||||
public static func calculate(data: String, pubKey: String, timestamp: UInt64, ttl: UInt64) -> String? {
|
||||
let payload = createPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl)
|
||||
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(LokiAPI.powDifficulty))
|
||||
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(SnodeAPI.powDifficulty))
|
||||
|
||||
// Start with the max value
|
||||
var trialValue = UInt64.max
|
||||
|
|
|
@ -37,19 +37,19 @@ internal class LokiFileServerProxy : LokiHTTPClient {
|
|||
}
|
||||
|
||||
// MARK: Proxying
|
||||
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
|
||||
let isLokiFileServer = (server == LokiFileServerAPI.server)
|
||||
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
|
||||
let isLokiFileServer = (server == FileServerAPI.server)
|
||||
guard isLokiFileServer else { return super.perform(request, withCompletionQueue: queue) } // Don't proxy open group requests for now
|
||||
return performLokiFileServerNSURLRequest(request, withCompletionQueue: queue)
|
||||
}
|
||||
|
||||
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
|
||||
internal func performLokiFileServerNSURLRequest(_ request: NSURLRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
|
||||
var headers = getCanonicalHeaders(for: request)
|
||||
return Promise<LokiAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
|
||||
return Promise<SnodeAPI.RawResponse> { [server = self.server, keyPair = self.keyPair, httpSession = self.httpSession] seal in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: LokiFileServerProxy.fileServerPublicKey, privateKey: keyPair.privateKey)
|
||||
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
|
||||
LokiAPI.getRandomSnode().then2 { proxy -> Promise<Any> in
|
||||
SnodeAPI.getRandomSnode().then2 { 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 }
|
||||
|
@ -84,7 +84,7 @@ internal class LokiFileServerProxy : LokiHTTPClient {
|
|||
"Connection" : "close", // TODO: Is this necessary?
|
||||
"Content-Type" : "application/json"
|
||||
]
|
||||
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
|
||||
let (promise, resolver) = SnodeAPI.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)!
|
||||
|
|
|
@ -14,8 +14,8 @@ public class LokiHTTPClient {
|
|||
return result
|
||||
}()
|
||||
|
||||
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> LokiAPI.RawResponsePromise in
|
||||
internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> SnodeAPI.RawResponsePromise {
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: queue).map2 { $0.responseObject }.recover2 { error -> SnodeAPI.RawResponsePromise in
|
||||
throw HTTPError.from(error: error) ?? error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import PromiseKit
|
||||
import SessionMetadataKit
|
||||
|
||||
/// Base class for `LokiFileServerAPI` and `LokiPublicChatAPI`.
|
||||
public class LokiDotNetAPI : NSObject {
|
||||
/// Base class for `FileServerAPI` and `PublicChatAPI`.
|
||||
public class DotNetAPI : NSObject {
|
||||
|
||||
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
internal static var userKeyPair: ECKeyPair { OWSIdentityManager.shared().identityKeyPair()! }
|
||||
|
@ -11,17 +11,18 @@ public class LokiDotNetAPI : NSObject {
|
|||
private static let attachmentType = "network.loki"
|
||||
|
||||
// MARK: Error
|
||||
@objc public class LokiDotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
@objc(LKDotNetAPIError)
|
||||
public class DotNetAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let generic = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
|
||||
@objc public static let parsingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
|
||||
@objc public static let signingFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
|
||||
@objc public static let encryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
|
||||
@objc public static let decryptionFailed = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
|
||||
@objc public static let maxFileSizeExceeded = LokiDotNetAPIError(domain: "LokiDotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
|
||||
@objc public static let generic = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "An error occurred." ])
|
||||
@objc public static let parsingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Invalid file server response." ])
|
||||
@objc public static let signingFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Couldn't sign message." ])
|
||||
@objc public static let encryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Couldn't encrypt file." ])
|
||||
@objc public static let decryptionFailed = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Couldn't decrypt file." ])
|
||||
@objc public static let maxFileSizeExceeded = DotNetAPIError(domain: "DotNetAPIErrorDomain", code: 6, userInfo: [ NSLocalizedDescriptionKey : "Maximum file size exceeded." ])
|
||||
}
|
||||
|
||||
// MARK: Database
|
||||
// MARK: Storage
|
||||
/// To be overridden by subclasses.
|
||||
internal class var authTokenCollection: String { preconditionFailure("authTokenCollection is abstract and must be overridden.") }
|
||||
|
||||
|
@ -70,7 +71,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
// Discard the "05" prefix if needed
|
||||
if serverPublicKey.count == 33 {
|
||||
|
@ -80,7 +81,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
// The challenge is prefixed by the 16 bit IV
|
||||
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
|
||||
let token = String(bytes: tokenAsData, encoding: .utf8) else {
|
||||
throw LokiDotNetAPIError.decryptionFailed
|
||||
throw DotNetAPIError.decryptionFailed
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
@ -101,14 +102,14 @@ public class LokiDotNetAPI : NSObject {
|
|||
}
|
||||
|
||||
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
|
||||
let isEncryptionRequired = (server == LokiFileServerAPI.server)
|
||||
let isEncryptionRequired = (server == FileServerAPI.server)
|
||||
return Promise<Void>() { seal in
|
||||
func proceed(with token: String) {
|
||||
// Get the attachment
|
||||
let data: Data
|
||||
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
|
||||
print("[Loki] Couldn't read attachment from disk.")
|
||||
return seal.reject(LokiDotNetAPIError.generic)
|
||||
return seal.reject(DotNetAPIError.generic)
|
||||
}
|
||||
// Encrypt the attachment if needed
|
||||
if isEncryptionRequired {
|
||||
|
@ -116,7 +117,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
var digest = NSData()
|
||||
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, outKey: &encryptionKey, outDigest: &digest) else {
|
||||
print("[Loki] Couldn't encrypt attachment.")
|
||||
return seal.reject(LokiDotNetAPIError.encryptionFailed)
|
||||
return seal.reject(DotNetAPIError.encryptionFailed)
|
||||
}
|
||||
attachment.encryptionKey = encryptionKey as Data
|
||||
attachment.digest = digest as Data
|
||||
|
@ -125,9 +126,9 @@ public class LokiDotNetAPI : NSObject {
|
|||
data = unencryptedAttachmentData
|
||||
}
|
||||
// Check the file size if needed
|
||||
let isLokiFileServer = (server == LokiFileServerAPI.server)
|
||||
if isLokiFileServer && data.count > LokiFileServerAPI.maxFileSize {
|
||||
return seal.reject(LokiDotNetAPIError.maxFileSizeExceeded)
|
||||
let isLokiFileServer = (server == FileServerAPI.server)
|
||||
if isLokiFileServer && data.count > FileServerAPI.maxFileSize {
|
||||
return seal.reject(DotNetAPIError.maxFileSizeExceeded)
|
||||
}
|
||||
// Create the request
|
||||
let url = "\(server)/files"
|
||||
|
@ -146,7 +147,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
// Parse the server ID & download URL
|
||||
guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
|
||||
print("[Loki] Couldn't parse attachment from: \(responseObject).")
|
||||
return seal.reject(LokiDotNetAPIError.parsingFailed)
|
||||
return seal.reject(DotNetAPIError.parsingFailed)
|
||||
}
|
||||
// Update the attachment
|
||||
attachment.serverId = serverID
|
||||
|
@ -155,7 +156,7 @@ public class LokiDotNetAPI : NSObject {
|
|||
attachment.save()
|
||||
seal.fulfill(())
|
||||
}
|
||||
let isProxyingRequired = (server == LokiFileServerAPI.server) // Don't proxy open group requests for now
|
||||
let isProxyingRequired = (server == FileServerAPI.server) // Don't proxy open group requests for now
|
||||
if isProxyingRequired {
|
||||
attachment.isUploaded = false
|
||||
attachment.save()
|
||||
|
@ -181,14 +182,14 @@ public class LokiDotNetAPI : NSObject {
|
|||
let isSuccessful = (200...299) ~= statusCode
|
||||
guard isSuccessful else {
|
||||
print("[Loki] Couldn't upload attachment.")
|
||||
return seal.reject(LokiDotNetAPIError.generic)
|
||||
return seal.reject(DotNetAPIError.generic)
|
||||
}
|
||||
parseResponse(responseObject)
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
if server == LokiFileServerAPI.server {
|
||||
if server == FileServerAPI.server {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
}
|
||||
|
@ -211,7 +212,7 @@ internal extension Promise {
|
|||
return recover2 { error -> Promise<T> in
|
||||
if let error = error as? NetworkManagerError, (error.statusCode == 401 || error.statusCode == 403) {
|
||||
print("[Loki] Group chat auth token for: \(server) expired; dropping it.")
|
||||
LokiDotNetAPI.clearAuthToken(for: server)
|
||||
DotNetAPI.clearAuthToken(for: server)
|
||||
}
|
||||
throw error
|
||||
}
|
|
@ -1,15 +1,16 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKFileServerAPI)
|
||||
public final class LokiFileServerAPI : LokiDotNetAPI {
|
||||
public final class FileServerAPI : DotNetAPI {
|
||||
|
||||
// MARK: Settings
|
||||
@objc public static let server = "https://file.getsession.org"
|
||||
public static let maxFileSize = 10_000_000 // 10 MB
|
||||
private static let deviceLinkType = "network.loki.messenger.devicemapping"
|
||||
private static let attachmentType = "net.app.core.oembed"
|
||||
|
||||
// MARK: Database
|
||||
public static let maxFileSize = 10_000_000 // 10 MB
|
||||
@objc public static let server = "https://file.getsession.org"
|
||||
|
||||
// MARK: Storage
|
||||
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }
|
||||
|
||||
// MARK: Device Links
|
||||
|
@ -41,7 +42,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
return Set(data.flatMap { data -> [DeviceLink] in
|
||||
guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] }
|
||||
|
@ -98,7 +99,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 attempt(maxRetryCount: 8, recoveringOn: LokiAPI.workQueue) {
|
||||
return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) {
|
||||
LokiFileServerProxy(for: server).perform(request).map2 { _ in }
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in
|
||||
print("Couldn't update device links due to error: \(error).")
|
||||
|
@ -138,7 +139,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
}
|
||||
|
||||
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
|
||||
guard profilePicture.count < maxFileSize else { return Promise(error: LokiDotNetAPIError.maxFileSizeExceeded) }
|
||||
guard profilePicture.count < maxFileSize else { return Promise(error: DotNetAPIError.maxFileSizeExceeded) }
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
|
@ -154,7 +155,7 @@ public final class LokiFileServerAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = Date()
|
||||
return downloadURL
|
|
@ -1,188 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
public extension LokiAPI {
|
||||
|
||||
fileprivate static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
|
||||
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodeFailureCount: [LokiAPITarget:UInt] = [:]
|
||||
// TODO: Read/write this directly from/to the database
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodePool: Set<LokiAPITarget> = []
|
||||
// TODO: Read/write this directly from/to the database
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var swarmCache: [String:[LokiAPITarget]] = [:]
|
||||
|
||||
// MARK: Settings
|
||||
private static let minimumSnodePoolCount = 32
|
||||
private static let minimumSwarmSnodeCount = 2
|
||||
private static let targetSwarmSnodeCount = 2
|
||||
|
||||
internal static let snodeFailureThreshold = 2
|
||||
|
||||
// MARK: Internal API
|
||||
internal static func getRandomSnode() -> Promise<LokiAPITarget> {
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
snodePool = storage.getSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
let target = seedNodePool.randomElement()!
|
||||
let url = "\(target)/json_rpc"
|
||||
let parameters: JSON = [
|
||||
"method" : "get_n_service_nodes",
|
||||
"params" : [
|
||||
"active_only" : true,
|
||||
"fields" : [
|
||||
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
|
||||
]
|
||||
]
|
||||
]
|
||||
print("[Loki] Populating snode pool using: \(target).")
|
||||
let (promise, seal) = Promise<LokiAPITarget>.pending()
|
||||
attempt(maxRetryCount: 4, recoveringOn: LokiAPI.workQueue) {
|
||||
HTTP.execute(.post, url, parameters: parameters).map2 { json -> LokiAPITarget in
|
||||
guard let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
|
||||
snodePool = try Set(rawTargets.flatMap { rawTarget in
|
||||
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawTarget).")
|
||||
return nil
|
||||
}
|
||||
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
})
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
return snodePool.randomElement()!
|
||||
}
|
||||
}.done2 { snode in
|
||||
seal.fulfill(snode)
|
||||
try! Storage.writeSync { transaction in
|
||||
print("[Loki] Persisting snode pool to database.")
|
||||
storage.setSnodePool(LokiAPI.snodePool, in: transaction)
|
||||
}
|
||||
}.catch2 { error in
|
||||
print("[Loki] Failed to contact seed node at: \(target).")
|
||||
seal.reject(error)
|
||||
}
|
||||
return promise
|
||||
} else {
|
||||
return Promise<LokiAPITarget> { seal in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
seal.fulfill(snodePool.randomElement()!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getSwarm(for hexEncodedPublicKey: String, isForcedReload: Bool = false) -> Promise<[LokiAPITarget]> {
|
||||
if swarmCache[hexEncodedPublicKey] == nil {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
swarmCache[hexEncodedPublicKey] = storage.getSwarm(for: hexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
|
||||
return Promise<[LokiAPITarget]> { $0.fulfill(cachedSwarm) }
|
||||
} else {
|
||||
print("[Loki] Getting swarm for: \(hexEncodedPublicKey).")
|
||||
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ]
|
||||
return getRandomSnode().then2 {
|
||||
invoke(.getSwarm, on: $0, associatedWith: hexEncodedPublicKey, parameters: parameters)
|
||||
}.map2 { rawSnodes in
|
||||
let swarm = parseTargets(from: rawSnodes)
|
||||
swarmCache[hexEncodedPublicKey] = swarm
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: hexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
return swarm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise<[LokiAPITarget]> {
|
||||
// shuffled() uses the system's default random generator, which is cryptographically secure
|
||||
return getSwarm(for: hexEncodedPublicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSnodePool(_ target: LokiAPITarget) {
|
||||
LokiAPI.snodePool.remove(target)
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.dropSnodeFromSnodePool(target, in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSwarmIfNeeded(_ target: LokiAPITarget, hexEncodedPublicKey: String) {
|
||||
let swarm = LokiAPI.swarmCache[hexEncodedPublicKey]
|
||||
if var swarm = swarm, let index = swarm.firstIndex(of: target) {
|
||||
swarm.remove(at: index)
|
||||
LokiAPI.swarmCache[hexEncodedPublicKey] = swarm
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: hexEncodedPublicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
@objc public static func clearSnodePool() {
|
||||
snodePool.removeAll()
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.clearSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Parsing
|
||||
private static func parseTargets(from rawResponse: Any) -> [LokiAPITarget] {
|
||||
guard let json = rawResponse as? JSON, let rawTargets = json["snodes"] as? [JSON] else {
|
||||
print("[Loki] Failed to parse targets from: \(rawResponse).")
|
||||
return []
|
||||
}
|
||||
return rawTargets.flatMap { rawTarget in
|
||||
guard let address = rawTarget["ip"] as? String, let portAsString = rawTarget["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawTarget).")
|
||||
return nil
|
||||
}
|
||||
return LokiAPITarget(address: "https://\(address)", port: port, publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Snode Error Handling
|
||||
internal extension Promise {
|
||||
|
||||
internal func handlingSnodeErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
|
||||
return recover2 { error -> Promise<T> in
|
||||
if let error = error as? LokiHTTPClient.HTTPError {
|
||||
switch error.statusCode {
|
||||
case 0, 400, 500, 503:
|
||||
// The snode is unreachable
|
||||
let oldFailureCount = LokiAPI.snodeFailureCount[target] ?? 0
|
||||
let newFailureCount = oldFailureCount + 1
|
||||
LokiAPI.snodeFailureCount[target] = newFailureCount
|
||||
print("[Loki] Couldn't reach snode at: \(target); setting failure count to \(newFailureCount).")
|
||||
if newFailureCount >= LokiAPI.snodeFailureThreshold {
|
||||
print("[Loki] Failure threshold reached for: \(target); dropping it.")
|
||||
LokiAPI.dropSnodeFromSwarmIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
|
||||
LokiAPI.dropSnodeFromSnodePool(target)
|
||||
LokiAPI.snodeFailureCount[target] = 0
|
||||
}
|
||||
case 406:
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
throw LokiAPI.LokiAPIError.clockOutOfSync
|
||||
case 421:
|
||||
// The snode isn't associated with the given public key anymore
|
||||
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")
|
||||
LokiAPI.dropSnodeFromSwarmIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey)
|
||||
case 432:
|
||||
// The PoW difficulty is too low
|
||||
if case LokiHTTPClient.HTTPError.networkError(_, let result, _) = error, let json = result as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
LokiAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty.")
|
||||
}
|
||||
break
|
||||
default: break
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKAPI)
|
||||
public final class LokiAPI : NSObject {
|
||||
internal static let workQueue = DispatchQueue(label: "LokiAPI.workQueue", qos: .userInitiated)
|
||||
|
||||
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
|
||||
// MARK: Settings
|
||||
private static let maxRetryCount: UInt = 4
|
||||
private static let defaultTimeout: TimeInterval = 20
|
||||
|
||||
internal static var powDifficulty: UInt = 1
|
||||
/// - Note: Changing this on the fly is not recommended.
|
||||
internal static var useOnionRequests = true
|
||||
|
||||
// MARK: Types
|
||||
public typealias RawResponse = Any
|
||||
|
||||
@objc public class LokiAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let proofOfWorkCalculationFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
|
||||
@objc public static let messageConversionFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
|
||||
@objc public static let clockOutOfSync = LokiAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
|
||||
@objc public static let randomSnodePoolUpdatingFailed = LokiAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
|
||||
@objc public static let missingSnodeVersion = LokiAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
|
||||
}
|
||||
|
||||
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
|
||||
|
||||
public typealias RawResponsePromise = Promise<RawResponse>
|
||||
|
||||
// MARK: Lifecycle
|
||||
override private init() { }
|
||||
|
||||
// MARK: Internal API
|
||||
internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String,
|
||||
parameters: JSON, headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
|
||||
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/v1")!
|
||||
if useOnionRequests {
|
||||
return OnionRequestAPI.sendOnionRequest(invoking: method, on: target, with: parameters, associatedWith: hexEncodedPublicKey).map2 { $0 as Any }
|
||||
} else {
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ])
|
||||
if let headers = headers { request.allHTTPHeaderFields = headers }
|
||||
request.timeoutInterval = timeout ?? defaultTimeout
|
||||
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global(qos: .default))
|
||||
.map2 { $0.responseObject }
|
||||
.handlingSnodeErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
|
||||
.recoveringNetworkErrorsIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getRawMessages(from target: LokiAPITarget) -> RawResponsePromise {
|
||||
let lastHashValue = getLastMessageHashValue(for: target) ?? ""
|
||||
let parameters = [ "pubKey" : getUserHexEncodedPublicKey(), "lastHash" : lastHashValue ]
|
||||
return invoke(.getMessages, on: target, associatedWith: getUserHexEncodedPublicKey(), parameters: parameters)
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
public static func getMessages() -> Promise<Set<MessageListPromise>> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) {
|
||||
getTargetSnodes(for: getUserHexEncodedPublicKey()).mapValues2 { targetSnode in
|
||||
getRawMessages(from: targetSnode).map2 { parseRawMessagesResponse($0, from: targetSnode) }
|
||||
}.map2 { Set($0) }
|
||||
}
|
||||
}
|
||||
|
||||
@objc(sendSignalMessage:onP2PSuccess:)
|
||||
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> AnyPromise {
|
||||
let promise = sendSignalMessage(signalMessage, onP2PSuccess: onP2PSuccess).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
|
||||
return AnyPromise.from(promise)
|
||||
}
|
||||
|
||||
public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise<Set<RawResponsePromise>> {
|
||||
// Convert the message to a Loki message
|
||||
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: LokiAPIError.messageConversionFailed) }
|
||||
let notificationCenter = NotificationCenter.default
|
||||
let destination = lokiMessage.destination
|
||||
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Calculate proof of work
|
||||
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
|
||||
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Get the target snodes
|
||||
return getTargetSnodes(for: destination).map2 { snodes in
|
||||
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
|
||||
let parameters = lokiMessageWithPoW.toJSON()
|
||||
return Set(snodes.map { snode in
|
||||
// Send the message to the target snode
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: LokiAPI.workQueue) {
|
||||
invoke(.sendMessage, on: snode, associatedWith: destination, parameters: parameters)
|
||||
}.map2 { rawResponse in
|
||||
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||
guard powDifficulty != LokiAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
LokiAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
|
||||
}
|
||||
return rawResponse
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
|
||||
|
||||
internal static func parseRawMessagesResponse(_ rawResponse: Any, from target: LokiAPITarget) -> [SSKProtoEnvelope] {
|
||||
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
|
||||
updateLastMessageHashValueIfPossible(for: target, from: rawMessages)
|
||||
let newRawMessages = removeDuplicates(from: rawMessages)
|
||||
let newMessages = parseProtoEnvelopes(from: newRawMessages)
|
||||
let newMessageCount = newMessages.count
|
||||
return newMessages
|
||||
}
|
||||
|
||||
private static func updateLastMessageHashValueIfPossible(for target: LokiAPITarget, from rawMessages: [JSON]) {
|
||||
if let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? Int {
|
||||
setLastMessageHashValue(for: target, hashValue: hashValue, expirationDate: UInt64(expirationDate))
|
||||
// FIXME: Move this out of here
|
||||
if UserDefaults.standard[.isUsingFullAPNs] {
|
||||
LokiPushNotificationManager.acknowledgeDelivery(forMessageWithHash: hashValue, expiration: expirationDate, hexEncodedPublicKey: getUserHexEncodedPublicKey())
|
||||
}
|
||||
} else if (!rawMessages.isEmpty) {
|
||||
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeDuplicates(from rawMessages: [JSON]) -> [JSON] {
|
||||
var receivedMessageHashValues = getReceivedMessageHashValues() ?? []
|
||||
return rawMessages.filter { rawMessage in
|
||||
guard let hashValue = rawMessage["hash"] as? String else {
|
||||
print("[Loki] Missing hash value for message: \(rawMessage).")
|
||||
return false
|
||||
}
|
||||
let isDuplicate = receivedMessageHashValues.contains(hashValue)
|
||||
receivedMessageHashValues.insert(hashValue)
|
||||
setReceivedMessageHashValues(to: receivedMessageHashValues)
|
||||
return !isDuplicate
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
|
||||
return rawMessages.compactMap { rawMessage in
|
||||
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
|
||||
print("[Loki] Failed to decode data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
guard let envelope = try? LokiMessageWrapper.unwrap(data: data) else {
|
||||
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
return envelope
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Message Hash Caching
|
||||
private static func getLastMessageHashValue(for target: LokiAPITarget) -> String? {
|
||||
var result: String? = nil
|
||||
// Uses a read/write connection because getting the last message hash value also removes expired messages as needed
|
||||
// TODO: This shouldn't be the case; a getter shouldn't have an unexpected side effect
|
||||
try! Storage.writeSync { transaction in
|
||||
result = storage.getLastMessageHash(forSnode: target.address, transaction: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expirationDate: UInt64) {
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.setLastMessageHash(forSnode: 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
|
||||
result = transaction.object(forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection) as! Set<String>?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set<String>) {
|
||||
try! Storage.writeSync { transaction in
|
||||
transaction.setObject(receivedMessageHashValues, forKey: receivedMessageHashValuesKey, inCollection: receivedMessageHashValuesCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Error Handling
|
||||
private extension Promise {
|
||||
|
||||
fileprivate func recoveringNetworkErrorsIfNeeded() -> Promise<T> {
|
||||
return recover2 { error -> Promise<T> in
|
||||
switch error {
|
||||
case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError
|
||||
case LokiHTTPClient.HTTPError.networkError(_, _, let underlyingError): throw underlyingError ?? error
|
||||
default: throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import PromiseKit
|
||||
|
||||
public struct LokiMessage {
|
||||
/// The hex encoded public key of the receiver.
|
||||
let destination: String
|
||||
/// The hex encoded public key of the recipient.
|
||||
let recipientPublicKey: String
|
||||
/// The content of the message.
|
||||
let data: LosslessStringConvertible
|
||||
/// The time to live for the message in milliseconds.
|
||||
|
@ -19,7 +19,7 @@ public struct LokiMessage {
|
|||
private(set) var nonce: String? = nil
|
||||
|
||||
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) {
|
||||
self.destination = destination
|
||||
self.recipientPublicKey = destination
|
||||
self.data = data
|
||||
self.ttl = ttl
|
||||
self.isPing = isPing
|
||||
|
@ -31,7 +31,7 @@ public struct LokiMessage {
|
|||
public static func from(signalMessage: SignalMessage) -> LokiMessage? {
|
||||
// To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object
|
||||
do {
|
||||
let wrappedMessage = try LokiMessageWrapper.wrap(message: signalMessage)
|
||||
let wrappedMessage = try MessageWrapper.wrap(message: signalMessage)
|
||||
let data = wrappedMessage.base64EncodedString()
|
||||
let destination = signalMessage.recipientPublicKey
|
||||
var ttl = TTLUtilities.fallbackMessageTTL
|
||||
|
@ -52,20 +52,20 @@ public struct LokiMessage {
|
|||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let now = NSDate.ows_millisecondTimeStamp()
|
||||
let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented
|
||||
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) {
|
||||
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.recipientPublicKey, timestamp: now, ttl: self.ttl) {
|
||||
var result = self
|
||||
result.timestamp = now
|
||||
result.nonce = nonce
|
||||
seal.fulfill(result)
|
||||
} else {
|
||||
seal.reject(LokiAPI.LokiAPIError.proofOfWorkCalculationFailed)
|
||||
seal.reject(SnodeAPI.SnodeAPIError.proofOfWorkCalculationFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toJSON() -> JSON {
|
||||
var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ]
|
||||
var result = [ "pubKey" : recipientPublicKey, "data" : data.description, "ttl" : String(ttl) ]
|
||||
if let timestamp = timestamp, let nonce = nonce {
|
||||
result["timestamp"] = String(timestamp)
|
||||
result["nonce"] = nonce
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
public enum LokiMessageWrapper {
|
||||
public enum MessageWrapper {
|
||||
|
||||
public enum Error : LocalizedError {
|
||||
case failedToWrapData
|
|
@ -18,7 +18,7 @@ extension OnionRequestAPI {
|
|||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
private static func encrypt(_ plaintext: Data, forSnode snode: LokiAPITarget) throws -> EncryptionResult {
|
||||
private static func encrypt(_ plaintext: Data, forSnode snode: Snode) throws -> EncryptionResult {
|
||||
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") }
|
||||
guard let hexEncodedSnodeX25519PublicKey = snode.publicKeySet?.x25519Key else { throw Error.snodePublicKeySetMissing }
|
||||
let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey)
|
||||
|
@ -31,7 +31,7 @@ extension OnionRequestAPI {
|
|||
}
|
||||
|
||||
/// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request.
|
||||
internal static func encrypt(_ payload: JSON, forTargetSnode snode: LokiAPITarget) -> Promise<EncryptionResult> {
|
||||
internal static func encrypt(_ payload: JSON, forTargetSnode snode: Snode) -> Promise<EncryptionResult> {
|
||||
let (promise, seal) = Promise<EncryptionResult>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
|
@ -51,7 +51,7 @@ extension OnionRequestAPI {
|
|||
}
|
||||
|
||||
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
||||
internal static func encryptHop(from lhs: LokiAPITarget, to rhs: LokiAPITarget, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
|
||||
internal static func encryptHop(from lhs: Snode, to rhs: Snode, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
|
||||
let (promise, seal) = Promise<EncryptionResult>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let parameters: JSON = [
|
||||
|
|
|
@ -3,12 +3,12 @@ import PromiseKit
|
|||
|
||||
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||
public enum OnionRequestAPI {
|
||||
public static var guardSnodes: Set<LokiAPITarget> = []
|
||||
public static var guardSnodes: Set<Snode> = []
|
||||
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
|
||||
|
||||
private static var snodePool: Set<LokiAPITarget> {
|
||||
let unreliableSnodes = Set(LokiAPI.snodeFailureCount.keys)
|
||||
return LokiAPI.snodePool.subtracting(unreliableSnodes)
|
||||
private static var snodePool: Set<Snode> {
|
||||
let unreliableSnodes = Set(SnodeAPI.snodeFailureCount.keys)
|
||||
return SnodeAPI.snodePool.subtracting(unreliableSnodes)
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
@ -40,14 +40,14 @@ public enum OnionRequestAPI {
|
|||
}
|
||||
|
||||
// MARK: Path
|
||||
public typealias Path = [LokiAPITarget]
|
||||
public typealias Path = [Snode]
|
||||
|
||||
// MARK: Onion Building Result
|
||||
private typealias OnionBuildingResult = (guardSnode: LokiAPITarget, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data)
|
||||
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data)
|
||||
|
||||
// MARK: Private API
|
||||
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
|
||||
private static func testSnode(_ snode: LokiAPITarget) -> Promise<Void> {
|
||||
private static func testSnode(_ snode: Snode) -> Promise<Void> {
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let url = "\(snode.address):\(snode.port)/get_stats/v1"
|
||||
|
@ -69,22 +69,22 @@ public enum OnionRequestAPI {
|
|||
|
||||
/// Finds `guardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
|
||||
/// if not enough (reliable) snodes are available.
|
||||
private static func getGuardSnodes() -> Promise<Set<LokiAPITarget>> {
|
||||
private static func getGuardSnodes() -> Promise<Set<Snode>> {
|
||||
if guardSnodes.count >= guardSnodeCount {
|
||||
return Promise<Set<LokiAPITarget>> { $0.fulfill(guardSnodes) }
|
||||
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
|
||||
} else {
|
||||
print("[Loki] [Onion Request API] Populating guard snode cache.")
|
||||
return LokiAPI.getRandomSnode().then2 { _ -> Promise<Set<LokiAPITarget>> in // Just used to populate the snode pool
|
||||
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
|
||||
var unusedSnodes = snodePool // Sync on LokiAPI.workQueue
|
||||
guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes }
|
||||
func getGuardSnode() -> Promise<LokiAPITarget> {
|
||||
func getGuardSnode() -> Promise<Snode> {
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
guard let candidate = unusedSnodes.randomElement() else { return Promise<LokiAPITarget> { $0.reject(Error.insufficientSnodes) } }
|
||||
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
|
||||
unusedSnodes.remove(candidate) // All used snodes should be unique
|
||||
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
|
||||
// Loop until a reliable guard snode is found
|
||||
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
|
||||
withDelay(0.25, completionQueue: LokiAPI.workQueue) { getGuardSnode() }
|
||||
withDelay(0.25, completionQueue: SnodeAPI.workQueue) { getGuardSnode() }
|
||||
}
|
||||
}
|
||||
let promises = (0..<guardSnodeCount).map { _ in getGuardSnode() }
|
||||
|
@ -104,7 +104,7 @@ public enum OnionRequestAPI {
|
|||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .buildingPaths, object: nil)
|
||||
}
|
||||
return LokiAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
|
||||
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
|
||||
return getGuardSnodes().map2 { guardSnodes -> [Path] in
|
||||
var unusedSnodes = snodePool.subtracting(guardSnodes)
|
||||
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount
|
||||
|
@ -137,7 +137,7 @@ public enum OnionRequestAPI {
|
|||
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
|
||||
///
|
||||
/// - Note: Exposed for testing purposes.
|
||||
internal static func getPath(excluding snode: LokiAPITarget) -> Promise<Path> {
|
||||
internal static func getPath(excluding snode: Snode) -> Promise<Path> {
|
||||
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
|
||||
if paths.count < pathCount {
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
|
@ -167,13 +167,13 @@ public enum OnionRequestAPI {
|
|||
}
|
||||
}
|
||||
|
||||
private static func dropGuardSnode(_ snode: LokiAPITarget) {
|
||||
private static func dropGuardSnode(_ snode: Snode) {
|
||||
guardSnodes = guardSnodes.filter { $0 != snode }
|
||||
}
|
||||
|
||||
/// Builds an onion around `payload` and returns the result.
|
||||
private static func buildOnion(around payload: JSON, targetedAt snode: LokiAPITarget) -> Promise<OnionBuildingResult> {
|
||||
var guardSnode: LokiAPITarget!
|
||||
private static func buildOnion(around payload: JSON, targetedAt snode: Snode) -> Promise<OnionBuildingResult> {
|
||||
var guardSnode: Snode!
|
||||
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
|
||||
var encryptionResult: EncryptionResult!
|
||||
return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in
|
||||
|
@ -204,9 +204,9 @@ public enum OnionRequestAPI {
|
|||
|
||||
// MARK: Internal API
|
||||
/// Sends an onion request to `snode`. Builds new paths as needed.
|
||||
internal static func sendOnionRequest(invoking method: LokiAPITarget.Method, on snode: LokiAPITarget, with parameters: JSON, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
|
||||
internal static func sendOnionRequest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
|
||||
let (promise, seal) = Promise<JSON>.pending()
|
||||
var guardSnode: LokiAPITarget!
|
||||
var guardSnode: Snode!
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
||||
buildOnion(around: payload, targetedAt: snode).done2 { intermediate in
|
||||
|
@ -232,7 +232,7 @@ public enum OnionRequestAPI {
|
|||
let bodyAsString = json["body"] as? String, let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
if statusCode == 406 { // Clock out of sync
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
seal.reject(LokiAPI.LokiAPIError.clockOutOfSync)
|
||||
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
|
||||
} else {
|
||||
guard let bodyAsData = bodyAsString.data(using: .utf8),
|
||||
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
|
@ -254,51 +254,10 @@ public enum OnionRequestAPI {
|
|||
dropAllPaths() // A snode in the path is bad; retry with a different path
|
||||
dropGuardSnode(guardSnode)
|
||||
}
|
||||
promise.handlingErrorsIfNeeded(forTargetSnode: snode, associatedWith: hexEncodedPublicKey)
|
||||
promise.recover2 { error -> Promise<JSON> in
|
||||
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
|
||||
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Target Snode Error Handling
|
||||
private extension Promise where T == JSON {
|
||||
|
||||
func handlingErrorsIfNeeded(forTargetSnode snode: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
|
||||
return recover2 { error -> Promise<JSON> in // Must be invoked on LokiAPI.errorHandlingQueue
|
||||
// The code below is very similar to that in LokiAPI.handlingSnodeErrorsIfNeeded(for:associatedWith:), but unfortunately slightly
|
||||
// different due to the fact that OnionRequestAPI uses the newer HTTP API, whereas LokiAPI still uses TSNetworkManager
|
||||
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
|
||||
switch statusCode {
|
||||
case 0, 400, 500, 503:
|
||||
// The snode is unreachable
|
||||
let oldFailureCount = LokiAPI.snodeFailureCount[snode] ?? 0
|
||||
let newFailureCount = oldFailureCount + 1
|
||||
LokiAPI.snodeFailureCount[snode] = newFailureCount
|
||||
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
|
||||
if newFailureCount >= LokiAPI.snodeFailureThreshold {
|
||||
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
|
||||
LokiAPI.dropSnodeFromSwarmIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey)
|
||||
LokiAPI.dropSnodeFromSnodePool(snode)
|
||||
LokiAPI.snodeFailureCount[snode] = 0
|
||||
}
|
||||
case 406:
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
throw LokiAPI.LokiAPIError.clockOutOfSync
|
||||
case 421:
|
||||
// The snode isn't associated with the given public key anymore
|
||||
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")
|
||||
LokiAPI.dropSnodeFromSwarmIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey)
|
||||
case 432:
|
||||
// The proof of work difficulty is too low
|
||||
if let powDifficulty = json["difficulty"] as? Int {
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
LokiAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty.")
|
||||
}
|
||||
break
|
||||
default: break
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKPublicChatAPI)
|
||||
public final class LokiPublicChatAPI : LokiDotNetAPI {
|
||||
public final class LokiPublicChatAPI : DotNetAPI {
|
||||
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
|
||||
|
||||
@objc public static let defaultChats: [LokiPublicChat] = [] // Currently unused
|
||||
|
@ -94,7 +94,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
return rawMessages.flatMap { message in
|
||||
let isDeleted = (message["is_deleted"] as? Int == 1)
|
||||
|
@ -174,7 +174,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
print("[Loki] Sending message to public chat channel with ID: \(channel) on server: \(server).")
|
||||
let (promise, seal) = Promise<LokiPublicChatMessage>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(LokiDotNetAPIError.signingFailed) }
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(DotNetAPIError.signingFailed) }
|
||||
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<LokiPublicChatMessage> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
|
@ -189,7 +189,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return LokiPublicChatMessage(serverID: serverID, hexEncodedPublicKey: getUserHexEncodedPublicKey(), displayName: displayName, profilePicture: signedMessage.profilePicture, body: body, type: publicChatMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature)
|
||||
|
@ -220,7 +220,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
return deletions.flatMap { deletion in
|
||||
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
|
||||
|
@ -269,7 +269,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
try! Storage.writeSync { transaction in
|
||||
data.forEach { data in
|
||||
|
@ -398,7 +398,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
let countInfo = data["counts"] as? JSON,
|
||||
let memberCount = countInfo["subscribers"] as? Int else {
|
||||
print("[Loki] Couldn't parse info for public chat channel with ID: \(channel) on server: \(server) from: \(rawResponse).")
|
||||
throw LokiDotNetAPIError.parsingFailed
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
let storage = OWSPrimaryStorage.shared()
|
||||
try! Storage.writeSync { transaction in
|
||||
|
@ -460,7 +460,7 @@ public final class LokiPublicChatAPI : LokiDotNetAPI {
|
|||
return LokiFileServerProxy(for: server).perform(request).map(on: DispatchQueue.global(qos: .default)) { 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
|
||||
throw DotNetAPIError.parsingFailed
|
||||
}
|
||||
let moderatorsAsSet = Set(moderators);
|
||||
if self.moderators.keys.contains(server) {
|
||||
|
|
|
@ -8,7 +8,6 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
private var pollForModeratorsTimer: Timer? = nil
|
||||
private var pollForDisplayNamesTimer: Timer? = nil
|
||||
private var hasStarted = false
|
||||
private let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
|
||||
// MARK: Settings
|
||||
private let pollForNewMessagesInterval: TimeInterval = 4
|
||||
|
@ -56,7 +55,7 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
|
||||
public func pollForNewMessages() -> Promise<Void> {
|
||||
let publicChat = self.publicChat
|
||||
let userHexEncodedPublicKey = self.userHexEncodedPublicKey
|
||||
let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
|
||||
return LokiPublicChatAPI.getMessages(for: publicChat.channel, on: publicChat.server).done(on: DispatchQueue.global(qos: .default)) { messages in
|
||||
let uniqueHexEncodedPublicKeys = Set(messages.map { $0.hexEncodedPublicKey })
|
||||
func proceed() {
|
||||
|
@ -194,13 +193,13 @@ public final class LokiPublicChatPoller : NSObject {
|
|||
return timeSinceLastUpdate > MultiDeviceProtocol.deviceLinkUpdateInterval
|
||||
}
|
||||
if !hexEncodedPublicKeysToUpdate.isEmpty {
|
||||
LokiFileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in
|
||||
FileServerAPI.getDeviceLinks(associatedWith: hexEncodedPublicKeysToUpdate).done(on: DispatchQueue.global(qos: .default)) { _ in
|
||||
proceed()
|
||||
hexEncodedPublicKeysToUpdate.forEach {
|
||||
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy
|
||||
}
|
||||
}.catch(on: DispatchQueue.global(qos: .default)) { error in
|
||||
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
|
||||
if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed {
|
||||
// Don't immediately re-fetch in case of failure due to a parsing error
|
||||
hexEncodedPublicKeysToUpdate.forEach {
|
||||
MultiDeviceProtocol.lastDeviceLinkUpdate[$0] = Date() // TODO: Doing this from a global queue seems a bit iffy
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKPoller)
|
||||
public final class LokiPoller : NSObject {
|
||||
private let onMessagesReceived: ([SSKProtoEnvelope]) -> Void
|
||||
public final class Poller : NSObject {
|
||||
private let storage = OWSPrimaryStorage.shared()
|
||||
private var hasStarted = false
|
||||
private var hasStopped = false
|
||||
private var usedSnodes = Set<LokiAPITarget>()
|
||||
private var isPolling = false
|
||||
private var usedSnodes = Set<Snode>()
|
||||
private var pollCount = 0
|
||||
|
||||
// MARK: Settings
|
||||
private static let pollInterval: TimeInterval = 1
|
||||
private static let pollInterval: TimeInterval = 2
|
||||
private static let retryInterval: TimeInterval = 0.25
|
||||
/// After polling a given snode this many times we always switch to a new one.
|
||||
///
|
||||
|
@ -29,41 +27,32 @@ public final class LokiPoller : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public init(onMessagesReceived: @escaping ([SSKProtoEnvelope]) -> Void) {
|
||||
self.onMessagesReceived = onMessagesReceived
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
@objc public func startIfNeeded() {
|
||||
guard !hasStarted else { return }
|
||||
guard !isPolling else { return }
|
||||
print("[Loki] Started polling.")
|
||||
hasStarted = true
|
||||
hasStopped = false
|
||||
isPolling = true
|
||||
setUpPolling()
|
||||
}
|
||||
|
||||
@objc public func stopIfNeeded() {
|
||||
guard !hasStopped else { return }
|
||||
@objc public func stop() {
|
||||
print("[Loki] Stopped polling.")
|
||||
hasStarted = false
|
||||
hasStopped = true
|
||||
isPolling = false
|
||||
usedSnodes.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
private func setUpPolling() {
|
||||
guard !hasStopped else { return }
|
||||
LokiAPI.getSwarm(for: getUserHexEncodedPublicKey(), isForcedReload: true).then2 { [weak self] _ -> Promise<Void> in
|
||||
guard isPolling else { return }
|
||||
SnodeAPI.getSwarm(for: getUserHexEncodedPublicKey(), isForcedReload: true).then2 { [weak self] _ -> Promise<Void> in
|
||||
guard let strongSelf = self else { return Promise { $0.fulfill(()) } }
|
||||
strongSelf.usedSnodes.removeAll()
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
strongSelf.pollNextSnode(seal: seal)
|
||||
return promise
|
||||
}.ensure(on: DispatchQueue.main) { [weak self] in // Timers don't do well on background queues
|
||||
guard let strongSelf = self, !strongSelf.hasStopped else { return }
|
||||
Timer.scheduledTimer(withTimeInterval: LokiPoller.retryInterval, repeats: false) { _ in
|
||||
guard let strongSelf = self, strongSelf.isPolling else { return }
|
||||
Timer.scheduledTimer(withTimeInterval: Poller.retryInterval, repeats: false) { _ in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.setUpPolling()
|
||||
}
|
||||
|
@ -72,7 +61,7 @@ public final class LokiPoller : NSObject {
|
|||
|
||||
private func pollNextSnode(seal: Resolver<Void>) {
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let swarm = LokiAPI.swarmCache[userPublicKey] ?? []
|
||||
let swarm = SnodeAPI.swarmCache[userPublicKey] ?? []
|
||||
let unusedSnodes = Set(swarm).subtracting(usedSnodes)
|
||||
if !unusedSnodes.isEmpty {
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
|
@ -85,7 +74,7 @@ public final class LokiPoller : NSObject {
|
|||
self?.pollCount = 0
|
||||
} else {
|
||||
print("[Loki] Polling \(nextSnode) failed; dropping it and switching to next snode.")
|
||||
LokiAPI.dropSnodeFromSwarmIfNeeded(nextSnode, hexEncodedPublicKey: userPublicKey)
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(nextSnode, publicKey: userPublicKey)
|
||||
}
|
||||
self?.pollNextSnode(seal: seal)
|
||||
}
|
||||
|
@ -94,19 +83,30 @@ public final class LokiPoller : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func poll(_ target: LokiAPITarget, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
|
||||
guard !hasStopped else { return Promise { $0.fulfill(()) } }
|
||||
return LokiAPI.getRawMessages(from: target).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise<Void> in
|
||||
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise { $0.fulfill(()) } }
|
||||
let messages = LokiAPI.parseRawMessagesResponse(rawResponse, from: target)
|
||||
strongSelf.onMessagesReceived(messages)
|
||||
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
|
||||
guard isPolling else { return Promise { $0.fulfill(()) } }
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: DispatchQueue.main) { [weak self] rawResponse -> Promise<Void> in
|
||||
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
||||
let messages = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
|
||||
if !messages.isEmpty {
|
||||
print("[Loki] Received \(messages.count) new message(s).")
|
||||
}
|
||||
messages.forEach { message in
|
||||
do {
|
||||
let data = try message.serializedData()
|
||||
SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data)
|
||||
} catch {
|
||||
print("[Loki] Failed to deserialize envelope due to error: \(error).")
|
||||
}
|
||||
}
|
||||
strongSelf.pollCount += 1
|
||||
if strongSelf.pollCount == LokiPoller.maxPollCount {
|
||||
if strongSelf.pollCount == Poller.maxPollCount {
|
||||
throw Error.pollLimitReached
|
||||
} else {
|
||||
return withDelay(LokiPoller.pollInterval, completionQueue: LokiAPI.workQueue) {
|
||||
guard let strongSelf = self, !strongSelf.hasStopped else { return Promise { $0.fulfill(()) } }
|
||||
return strongSelf.poll(target, seal: longTermSeal)
|
||||
return withDelay(Poller.pollInterval, completionQueue: SnodeAPI.workQueue) {
|
||||
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
|
||||
return strongSelf.poll(snode, seal: longTermSeal)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ public enum LokiRSSFeedProxy {
|
|||
}
|
||||
|
||||
public static func fetchContent(for url: String) -> Promise<String> {
|
||||
let server = LokiFileServerAPI.server
|
||||
let server = FileServerAPI.server
|
||||
let endpoints = [ "messenger-updates/feed" : "loki/v1/rss/messenger", "loki.network/feed" : "loki/v1/rss/loki" ]
|
||||
let endpoint = endpoints.first { url.lowercased().contains($0.key) }!.value
|
||||
let url = URL(string: server + "/" + endpoint)!
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
|
||||
public typealias Snode = LokiAPITarget
|
||||
|
||||
/// Either a service node or another client if P2P is enabled.
|
||||
public final class LokiAPITarget : NSObject, NSCoding {
|
||||
public final class Snode : NSObject, NSCoding {
|
||||
public let address: String
|
||||
public let port: UInt16
|
||||
internal let publicKeySet: KeySet?
|
||||
|
@ -55,7 +52,7 @@ public final class LokiAPITarget : NSObject, NSCoding {
|
|||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? LokiAPITarget else { return false }
|
||||
guard let other = other as? Snode else { return false }
|
||||
return address == other.address && port == other.port
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKSnodeAPI)
|
||||
public final class SnodeAPI : NSObject {
|
||||
internal static let workQueue = DispatchQueue(label: "SnodeAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
|
||||
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodeFailureCount: [Snode:UInt] = [:]
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodePool: Set<Snode> = []
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var swarmCache: [String:[Snode]] = [:]
|
||||
|
||||
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
|
||||
// MARK: Settings
|
||||
internal static let snodeFailureThreshold = 2
|
||||
private static let maxRetryCount: UInt = 4
|
||||
private static let minimumSnodePoolCount = 32
|
||||
private static let minimumSwarmSnodeCount = 2
|
||||
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
|
||||
private static let targetSwarmSnodeCount = 2
|
||||
|
||||
internal static var powDifficulty: UInt = 1
|
||||
/// - Note: Changing this on the fly is not recommended.
|
||||
internal static var useOnionRequests = true
|
||||
|
||||
// MARK: Error
|
||||
@objc(LKSnodeAPIError)
|
||||
public class SnodeAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let proofOfWorkCalculationFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
|
||||
@objc public static let messageConversionFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
|
||||
@objc public static let clockOutOfSync = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
|
||||
@objc public static let randomSnodePoolUpdatingFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
|
||||
@objc public static let missingSnodeVersion = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
|
||||
}
|
||||
|
||||
// MARK: Type Aliases
|
||||
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
|
||||
public typealias RawResponse = Any
|
||||
public typealias RawResponsePromise = Promise<RawResponse>
|
||||
|
||||
// MARK: Lifecycle
|
||||
override private init() { }
|
||||
|
||||
// MARK: Core
|
||||
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
|
||||
if useOnionRequests {
|
||||
return OnionRequestAPI.sendOnionRequest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
|
||||
} else {
|
||||
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
|
||||
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
|
||||
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error }
|
||||
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getRandomSnode() -> Promise<Snode> {
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
snodePool = storage.getSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
let target = seedNodePool.randomElement()!
|
||||
let url = "\(target)/json_rpc"
|
||||
let parameters: JSON = [
|
||||
"method" : "get_n_service_nodes",
|
||||
"params" : [
|
||||
"active_only" : true,
|
||||
"fields" : [
|
||||
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
|
||||
]
|
||||
]
|
||||
]
|
||||
print("[Loki] Populating snode pool using: \(target).")
|
||||
let (promise, seal) = Promise<Snode>.pending()
|
||||
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
|
||||
HTTP.execute(.post, url, parameters: parameters).map2 { json -> Snode in
|
||||
guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw SnodeAPIError.randomSnodePoolUpdatingFailed }
|
||||
snodePool = try Set(rawSnodes.flatMap { rawSnode in
|
||||
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
|
||||
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawSnode).")
|
||||
return nil
|
||||
}
|
||||
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
})
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
return snodePool.randomElement()!
|
||||
}
|
||||
}.done2 { snode in
|
||||
seal.fulfill(snode)
|
||||
try! Storage.writeSync { transaction in
|
||||
print("[Loki] Persisting snode pool to database.")
|
||||
storage.setSnodePool(SnodeAPI.snodePool, in: transaction)
|
||||
}
|
||||
}.catch2 { error in
|
||||
print("[Loki] Failed to contact seed node at: \(target).")
|
||||
seal.reject(error)
|
||||
}
|
||||
return promise
|
||||
} else {
|
||||
return Promise<Snode> { seal in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
seal.fulfill(snodePool.randomElement()!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise<[Snode]> {
|
||||
if swarmCache[publicKey] == nil {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
swarmCache[publicKey] = storage.getSwarm(for: publicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
|
||||
return Promise<[Snode]> { $0.fulfill(cachedSwarm) }
|
||||
} else {
|
||||
print("[Loki] Getting swarm for: \(publicKey).")
|
||||
let parameters: [String:Any] = [ "pubKey" : publicKey ]
|
||||
return getRandomSnode().then2 {
|
||||
invoke(.getSwarm, on: $0, associatedWith: publicKey, parameters: parameters)
|
||||
}.map2 { rawSnodes in
|
||||
let swarm = parseSnodes(from: rawSnodes)
|
||||
swarmCache[publicKey] = swarm
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: publicKey, in: transaction)
|
||||
}
|
||||
return swarm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
|
||||
// shuffled() uses the system's default random generator, which is cryptographically secure
|
||||
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSnodePool(_ snode: Snode) {
|
||||
SnodeAPI.snodePool.remove(snode)
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.dropSnodeFromSnodePool(snode, in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func clearSnodePool() {
|
||||
snodePool.removeAll()
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.clearSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
|
||||
let swarm = SnodeAPI.swarmCache[publicKey]
|
||||
if var swarm = swarm, let index = swarm.firstIndex(of: snode) {
|
||||
swarm.remove(at: index)
|
||||
SnodeAPI.swarmCache[publicKey] = swarm
|
||||
try! Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: publicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Receiving
|
||||
internal static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
||||
try! Storage.writeSync { transaction in
|
||||
Storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction)
|
||||
}
|
||||
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey)
|
||||
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
|
||||
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||
}
|
||||
|
||||
public static func getMessages(for publicKey: String) -> Promise<Set<MessageListPromise>> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
|
||||
getTargetSnodes(for: publicKey).mapValues2 { targetSnode in
|
||||
getRawMessages(from: targetSnode, associatedWith: publicKey).map2 {
|
||||
parseRawMessagesResponse($0, from: targetSnode, associatedWith: publicKey)
|
||||
}
|
||||
}.map2 { Set($0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Sending
|
||||
@objc(sendSignalMessage:)
|
||||
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage) -> AnyPromise {
|
||||
let promise = sendSignalMessage(signalMessage).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
|
||||
return AnyPromise.from(promise)
|
||||
}
|
||||
|
||||
public static func sendSignalMessage(_ signalMessage: SignalMessage) -> Promise<Set<RawResponsePromise>> {
|
||||
// Convert the message to a Loki message
|
||||
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: SnodeAPIError.messageConversionFailed) }
|
||||
let publicKey = lokiMessage.recipientPublicKey
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Calculate proof of work
|
||||
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
|
||||
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Get the target snodes
|
||||
return getTargetSnodes(for: publicKey).map2 { snodes in
|
||||
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
|
||||
let parameters = lokiMessageWithPoW.toJSON()
|
||||
return Set(snodes.map { snode in
|
||||
// Send the message to the target snode
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
|
||||
invoke(.sendMessage, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||
}.map2 { rawResponse in
|
||||
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||
guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
|
||||
}
|
||||
return rawResponse
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
|
||||
|
||||
private static func parseSnodes(from rawResponse: Any) -> [Snode] {
|
||||
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
|
||||
print("[Loki] Failed to parse targets from: \(rawResponse).")
|
||||
return []
|
||||
}
|
||||
return rawSnodes.flatMap { rawSnode in
|
||||
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawSnode).")
|
||||
return nil
|
||||
}
|
||||
return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [SSKProtoEnvelope] {
|
||||
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
|
||||
if let (lastHash, expirationDate) = updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages),
|
||||
UserDefaults.standard[.isUsingFullAPNs] {
|
||||
LokiPushNotificationManager.acknowledgeDelivery(forMessageWithHash: lastHash, expiration: expirationDate, hexEncodedPublicKey: getUserHexEncodedPublicKey())
|
||||
}
|
||||
let rawNewMessages = removeDuplicates(from: rawMessages, associatedWith: publicKey)
|
||||
let newMessages = parseProtoEnvelopes(from: rawNewMessages)
|
||||
return newMessages
|
||||
}
|
||||
|
||||
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) -> (String, UInt64)? {
|
||||
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
|
||||
try! Storage.writeSync { transaction in
|
||||
Storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey, to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
|
||||
}
|
||||
return (lastHash, expirationDate)
|
||||
} else if (!rawMessages.isEmpty) {
|
||||
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
|
||||
var receivedMessages = Storage.getReceivedMessages(for: publicKey) ?? []
|
||||
return rawMessages.filter { rawMessage in
|
||||
guard let hash = rawMessage["hash"] as? String else {
|
||||
print("[Loki] Missing hash value for message: \(rawMessage).")
|
||||
return false
|
||||
}
|
||||
let isDuplicate = receivedMessages.contains(hash)
|
||||
receivedMessages.insert(hash)
|
||||
try! Storage.writeSync { transaction in
|
||||
Storage.setReceivedMessages(to: receivedMessages, for: publicKey, using: transaction)
|
||||
}
|
||||
return !isDuplicate
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
|
||||
return rawMessages.compactMap { rawMessage in
|
||||
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
|
||||
print("[Loki] Failed to decode data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
guard let envelope = try? MessageWrapper.unwrap(data: data) else {
|
||||
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
return envelope
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Error Handling
|
||||
/// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String) -> Error? {
|
||||
#if DEBUG
|
||||
assertOnQueue(SnodeAPI.workQueue)
|
||||
#endif
|
||||
switch statusCode {
|
||||
case 0, 400, 500, 503:
|
||||
// The snode is unreachable
|
||||
let oldFailureCount = SnodeAPI.snodeFailureCount[snode] ?? 0
|
||||
let newFailureCount = oldFailureCount + 1
|
||||
SnodeAPI.snodeFailureCount[snode] = newFailureCount
|
||||
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
|
||||
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
|
||||
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||
SnodeAPI.dropSnodeFromSnodePool(snode)
|
||||
SnodeAPI.snodeFailureCount[snode] = 0
|
||||
}
|
||||
case 406:
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
return SnodeAPI.SnodeAPIError.clockOutOfSync
|
||||
case 421:
|
||||
// The snode isn't associated with the given public key anymore
|
||||
print("[Loki] Invalidating swarm for: \(publicKey).")
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||
case 432:
|
||||
// The proof of work difficulty is too low
|
||||
if let powDifficulty = json?["difficulty"] as? UInt {
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty.")
|
||||
}
|
||||
break
|
||||
default: break
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
internal extension Storage {
|
||||
|
||||
// MARK: Last Message Hash
|
||||
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
|
||||
|
||||
internal static func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
var result: JSON?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: key, inCollection: lastMessageHashCollection) as? JSON
|
||||
}
|
||||
if let result = result {
|
||||
guard result["hash"] as? String != nil else { return nil }
|
||||
guard result["expirationDate"] as? NSNumber != nil else { return nil }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey),
|
||||
let hash = lastMessageHashInfo["hash"] as? String, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return }
|
||||
let now = NSDate.ows_millisecondTimeStamp()
|
||||
if now >= expirationDate {
|
||||
removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? {
|
||||
return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String
|
||||
}
|
||||
|
||||
internal static func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
transaction.removeObject(forKey: key, inCollection: lastMessageHashCollection)
|
||||
}
|
||||
|
||||
internal static func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let key = "\(snode.address):\(snode.port).\(publicKey)"
|
||||
guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return }
|
||||
transaction.setObject(lastMessageHashInfo, forKey: key, inCollection: lastMessageHashCollection)
|
||||
}
|
||||
|
||||
// MARK: Received Messages
|
||||
private static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
|
||||
|
||||
internal static func getReceivedMessages(for publicKey: String) -> Set<String>? {
|
||||
var result: Set<String>?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: publicKey, inCollection: receivedMessagesCollection) as? Set<String>
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.setObject(receivedMessages, forKey: publicKey, inCollection: receivedMessagesCollection)
|
||||
}
|
||||
}
|
|
@ -154,37 +154,6 @@
|
|||
[LKLogger print:[NSString stringWithFormat:@"[Loki] Removed pre key bundle from: %@.", hexEncodedPublicKey]];
|
||||
}
|
||||
|
||||
# pragma mark - Last Message Hash
|
||||
|
||||
#define LKLastMessageHashCollection @"LKLastMessageHashCollection"
|
||||
|
||||
- (NSString *_Nullable)getLastMessageHashForSnode:(NSString *)snode transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
NSDictionary *_Nullable dict = [transaction objectForKey:snode inCollection:LKLastMessageHashCollection];
|
||||
if (dict == nil) { return nil; }
|
||||
|
||||
NSString *_Nullable hash = dict[@"hash"];
|
||||
if (hash == nil) { return nil; }
|
||||
|
||||
// Check if the hash has expired
|
||||
uint64_t now = NSDate.ows_millisecondTimeStamp;
|
||||
NSNumber *_Nullable expiresAt = dict[@"expiresAt"];
|
||||
if (expiresAt && expiresAt.unsignedLongLongValue <= now) {
|
||||
[self removeLastMessageHashForSnode:snode transaction:transaction];
|
||||
return nil;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
- (void)setLastMessageHashForSnode:(NSString *)snode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
NSDictionary *dict = @{ @"hash" : hash, @"expiresAt": @(expiresAt) };
|
||||
[transaction setObject:dict forKey:snode inCollection:LKLastMessageHashCollection];
|
||||
}
|
||||
|
||||
- (void)removeLastMessageHashForSnode:(NSString *)snode transaction:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
[transaction removeObjectForKey:snode inCollection:LKLastMessageHashCollection];
|
||||
}
|
||||
|
||||
# pragma mark - Open Groups
|
||||
|
||||
#define LKMessageIDCollection @"LKMessageIDCollection"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
public extension OWSPrimaryStorage {
|
||||
|
||||
// MARK: Snode Pool
|
||||
public func setSnodePool(_ snodePool: Set<LokiAPITarget>, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
public func setSnodePool(_ snodePool: Set<Snode>, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
clearSnodePool(in: transaction)
|
||||
snodePool.forEach { snode in
|
||||
transaction.setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection)
|
||||
|
@ -15,16 +15,16 @@ public extension OWSPrimaryStorage {
|
|||
transaction.removeAllObjects(inCollection: Storage.snodePoolCollection)
|
||||
}
|
||||
|
||||
public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set<LokiAPITarget> {
|
||||
var result: Set<LokiAPITarget> = []
|
||||
public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set<Snode> {
|
||||
var result: Set<Snode> = []
|
||||
transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in
|
||||
guard let snode = object as? LokiAPITarget else { return }
|
||||
guard let snode = object as? Snode else { return }
|
||||
result.insert(snode)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func dropSnodeFromSnodePool(_ snode: LokiAPITarget, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
public func dropSnodeFromSnodePool(_ snode: Snode, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
transaction.removeObject(forKey: snode.description, inCollection: Storage.snodePoolCollection)
|
||||
}
|
||||
|
||||
|
@ -72,12 +72,12 @@ public extension OWSPrimaryStorage {
|
|||
public func getOnionRequestPaths(in transaction: YapDatabaseReadTransaction) -> [OnionRequestAPI.Path] {
|
||||
let collection = Storage.onionRequestPathCollection
|
||||
guard
|
||||
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? LokiAPITarget,
|
||||
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? LokiAPITarget,
|
||||
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? LokiAPITarget,
|
||||
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? LokiAPITarget,
|
||||
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? LokiAPITarget,
|
||||
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? LokiAPITarget else { return [] }
|
||||
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Snode,
|
||||
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Snode,
|
||||
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Snode,
|
||||
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Snode,
|
||||
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Snode,
|
||||
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Snode else { return [] }
|
||||
return [ [ path0Snode0, path0Snode1, path0Snode2 ], [ path1Snode0, path1Snode1, path1Snode2 ] ]
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKClosedGroupPoller)
|
||||
public final class ClosedGroupPoller : NSObject {
|
||||
private var isPolling = false
|
||||
private var timer: Timer?
|
||||
|
||||
// MARK: Settings
|
||||
private static let pollInterval: TimeInterval = 4
|
||||
|
||||
// MARK: Error
|
||||
private enum Error : LocalizedError {
|
||||
case insufficientSnodes
|
||||
case pollingCanceled
|
||||
|
||||
internal var errorDescription: String? {
|
||||
switch self {
|
||||
case .insufficientSnodes: return "No snodes left to poll."
|
||||
case .pollingCanceled: return "Polling canceled."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
@objc public func startIfNeeded() {
|
||||
AssertIsOnMainThread() // Timers don't do well on background queues
|
||||
guard !isPolling else { return }
|
||||
isPolling = true
|
||||
timer = Timer.scheduledTimer(withTimeInterval: ClosedGroupPoller.pollInterval, repeats: true) { [weak self] _ in
|
||||
self?.poll()
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func stop() {
|
||||
isPolling = false
|
||||
timer?.invalidate()
|
||||
}
|
||||
|
||||
// MARK: Private API
|
||||
private func poll() {
|
||||
guard isPolling else { return }
|
||||
let publicKeys = Storage.getUserClosedGroupPublicKeys()
|
||||
publicKeys.forEach { publicKey in
|
||||
SnodeAPI.getSwarm(for: publicKey).then2 { [weak self] swarm -> Promise<[SSKProtoEnvelope]> in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
|
||||
guard let self = self, self.isPolling else { return Promise(error: Error.pollingCanceled) }
|
||||
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).map2 {
|
||||
SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: publicKey)
|
||||
}
|
||||
}.done2 { [weak self] messages in
|
||||
guard let self = self, self.isPolling else { return }
|
||||
if !messages.isEmpty {
|
||||
print("[Loki] Received \(messages.count) new message(s) in closed group with public key: \(publicKey).")
|
||||
}
|
||||
messages.forEach { message in
|
||||
do {
|
||||
let data = try message.serializedData()
|
||||
SSKEnvironment.shared.messageReceiver.handleReceivedEnvelopeData(data)
|
||||
} catch {
|
||||
print("[Loki] Failed to deserialize envelope due to error: \(error).")
|
||||
}
|
||||
}
|
||||
}.catch2 { error in
|
||||
print("[Loki] Polling failed for closed group with public key: \(publicKey) due to error: \(error).")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,41 +1,49 @@
|
|||
|
||||
@objc(LKClosedGroupUpdateMessage)
|
||||
internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
|
||||
private let name: String
|
||||
private let id: String
|
||||
private let sharedSecret: String
|
||||
private let senderKey: String
|
||||
private let members: Set<String>
|
||||
private let kind: Kind
|
||||
|
||||
// MARK: Settings
|
||||
@objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) }
|
||||
|
||||
@objc internal override func shouldBeSaved() -> Bool { return false }
|
||||
@objc internal override func shouldSyncTranscript() -> Bool { return false }
|
||||
|
||||
@objc internal init(thread: TSThread, name: String, id: String, sharedSecret: String, senderKey: String, members: Set<String>) {
|
||||
self.name = name
|
||||
self.id = id
|
||||
self.sharedSecret = sharedSecret
|
||||
self.senderKey = senderKey
|
||||
self.members = members
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "Closed group created",
|
||||
// MARK: Kind
|
||||
internal enum Kind {
|
||||
case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, chainKeys: [Data], members: [String], admins: [String])
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
internal init(thread: TSThread, kind: Kind) {
|
||||
self.kind = kind
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "",
|
||||
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
|
||||
groupMetaMessage: .new, quotedMessage: nil, contactShare: nil, linkPreview: nil)
|
||||
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(thread:name:id:secretKey:senderKey:members:) instead.")
|
||||
preconditionFailure("Use init(thread:kind:) instead.")
|
||||
}
|
||||
|
||||
required init(dictionary: [String:Any]) throws {
|
||||
preconditionFailure("Use init(thread:name:id:secretKey:senderKey:members:) instead.")
|
||||
preconditionFailure("Use init(thread:kind:) instead.")
|
||||
}
|
||||
|
||||
// MARK: Building
|
||||
@objc internal override func dataMessageBuilder() -> Any? {
|
||||
guard let builder = super.dataMessageBuilder() as? SSKProtoDataMessage.SSKProtoDataMessageBuilder else { return nil }
|
||||
let closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(name: name, groupID: id, sharedSecret: sharedSecret, senderKey: senderKey)
|
||||
closedGroupUpdate.setMembers([String](members))
|
||||
do {
|
||||
let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder
|
||||
switch kind {
|
||||
case .new(let groupPublicKey, let name, let groupPrivateKey, let chainKeys, let members, let admins):
|
||||
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .new)
|
||||
closedGroupUpdate.setName(name)
|
||||
closedGroupUpdate.setGroupPrivateKey(groupPrivateKey)
|
||||
closedGroupUpdate.setChainKeys(chainKeys)
|
||||
closedGroupUpdate.setMembers(members)
|
||||
closedGroupUpdate.setAdmins(admins)
|
||||
}
|
||||
builder.setClosedGroupUpdate(try closedGroupUpdate.build())
|
||||
} catch {
|
||||
owsFailDebug("Failed to build closed group update due to error: \(error).")
|
||||
|
|
|
@ -16,6 +16,34 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
private static let gcmTagSize: UInt = 16
|
||||
private static let ivSize: UInt = 12
|
||||
|
||||
// A quick overview of how shared sender key based closed groups work:
|
||||
//
|
||||
// • When a user creates the group, they generate a key pair for the group along with a ratchet for
|
||||
// every member of the group. They bundle this together with some other group info such as the group
|
||||
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
|
||||
// the group. Note that because a user can only pick from their existing contacts when selecting
|
||||
// the group members they don't need to establish sessions before being able to send the
|
||||
// `ClosedGroupUpdateMessage`. Another way to optimize the performance of the group creation process
|
||||
// is to batch fetch the device links of all members involved ahead of time, rather than letting
|
||||
// the sending pipeline do it separately for every user the `ClosedGroupUpdateMessage` is sent to.
|
||||
// • After the group is created, every user polls for the public key associated with the group.
|
||||
// • Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
|
||||
// other members of the group they don't yet have a session with for reasons outlined below.
|
||||
// • When a user sends a message they step their ratchet and use the resulting message key to encrypt
|
||||
// the message.
|
||||
// • When another user receives that message, they step the ratchet associated with the sender and
|
||||
// use the resulting message key to decrypt the message.
|
||||
// • When a user leaves the group, new ratchets must be generated for all members to ensure that the
|
||||
// user that left can't decrypt messages going forward. To this end every user deletes all ratchets
|
||||
// associated with the group in question upon receiving a group update message that indicates that
|
||||
// a user left. They then generate a new ratchet for themselves and send it out to all members of
|
||||
// the group (again fetching device links ahead of time). The user should already have established
|
||||
// sessions with all other members at this point because of the behavior outlined a few points above.
|
||||
// • When a user adds a new member to the group, they generate a ratchet for that new member and
|
||||
// send that to all existing members of the group in a `ClosedGroupUpdateMessage`. They send a
|
||||
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
|
||||
// every other member of the group to the user that joined.
|
||||
|
||||
public struct Ratchet {
|
||||
public let chainKey: String
|
||||
public let keyIndex: UInt
|
||||
|
@ -23,46 +51,60 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
}
|
||||
|
||||
public enum RatchetingError : LocalizedError {
|
||||
case loadingFailed(groupID: String, senderPublicKey: String)
|
||||
case messageKeyMissing(targetKeyIndex: UInt, groupID: String, senderPublicKey: String)
|
||||
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
|
||||
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .loadingFailed(let groupID, let senderPublicKey): return "Couldn't get ratchet for closed group with ID: \(groupID), sender public key: \(senderPublicKey)."
|
||||
case .messageKeyMissing(let targetKeyIndex, let groupID, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), group ID: \(groupID), sender public key: \(senderPublicKey)."
|
||||
case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
|
||||
case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// - Note: It's recommended to batch fetch the device links for the given set of members before invoking this, to avoid
|
||||
/// the message sending pipeline making a request for each member.
|
||||
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) {
|
||||
// Generate a key pair for the group
|
||||
let keyPair = Curve25519.generateKeyPair()
|
||||
// The group ID is its public key (hex encoded)
|
||||
let groupID = keyPair.publicKey.toHexString()
|
||||
// Create the ratchet
|
||||
public static func createClosedGroup(name: String, members membersAsSet: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> TSGroupThread {
|
||||
var membersAsSet = membersAsSet
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let ratchet = generateRatchet(for: groupID, senderPublicKey: userPublicKey, transaction: transaction)
|
||||
// Get the shared secret and sender key to include in the closed group update message
|
||||
let sharedSecret = keyPair.privateKey.toHexString()
|
||||
let senderKey = ratchet.chainKey
|
||||
// Generate a key pair for the group
|
||||
let groupKeyPair = Curve25519.generateKeyPair()
|
||||
let groupPublicKey = groupKeyPair.publicKey.toHexString()
|
||||
// Ensure the current user's master device is included in the member list
|
||||
membersAsSet.remove(userPublicKey)
|
||||
membersAsSet.insert(UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey)
|
||||
// Create ratchets for all users involved
|
||||
let members = [String](membersAsSet)
|
||||
let ratchets = members.map { generateRatchet(for: groupPublicKey, senderPublicKey: $0, transaction: transaction) }
|
||||
// Create the group
|
||||
let admins = [ UserDefaults.standard[.masterHexEncodedPublicKey] ?? userPublicKey ]
|
||||
let wrappedGroupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupID)
|
||||
let group = TSGroupModel(title: name, memberIds: [String](members), image: nil, groupId: wrappedGroupID, groupType: .closedGroup, adminIds: admins)
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)
|
||||
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.usesSharedSenderKeys = true
|
||||
thread.save(with: transaction)
|
||||
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
|
||||
// Send a closed group update message to all members involved
|
||||
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, name: name, id: groupID, sharedSecret: sharedSecret, senderKey: senderKey, members: members)
|
||||
let chainKeys = ratchets.map { Data(hex: $0.chainKey) }
|
||||
let closedGroupUpdateMessageKind = ClosedGroupUpdateMessage.Kind.new(groupPublicKey: groupKeyPair.publicKey, name: name, groupPrivateKey: groupKeyPair.privateKey, chainKeys: chainKeys, members: members, admins: admins)
|
||||
let closedGroupUpdateMessage = ClosedGroupUpdateMessage(thread: thread, kind: closedGroupUpdateMessageKind)
|
||||
let messageSenderJobQueue = SSKEnvironment.shared.messageSenderJobQueue
|
||||
messageSenderJobQueue.add(message: closedGroupUpdateMessage, transaction: transaction)
|
||||
// Store the group's key pair
|
||||
Storage.addClosedGroupKeyPair(groupKeyPair)
|
||||
// Notify the user
|
||||
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
|
||||
infoMessage.save(with: transaction)
|
||||
// The user can only pick from existing contacts when selecting closed group
|
||||
// members so there's no need to establish sessions
|
||||
// Return
|
||||
return thread
|
||||
}
|
||||
|
||||
private static func generateRatchet(for groupID: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Ratchet {
|
||||
let rootChainKey = Randomness.generateRandomBytes(32)!.toHexString()
|
||||
private static func generateRatchet(for groupPublicKey: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) -> Ratchet {
|
||||
let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString()
|
||||
let ratchet = Ratchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: [])
|
||||
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: ratchet, transaction: transaction)
|
||||
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, transaction: transaction)
|
||||
return ratchet
|
||||
}
|
||||
|
||||
|
@ -74,18 +116,18 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
private static func stepRatchetOnce(for groupID: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
|
||||
private static func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
|
||||
#if DEBUG
|
||||
assert(!Thread.isMainThread)
|
||||
#endif
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey) else {
|
||||
let error = RatchetingError.loadingFailed(groupID: groupID, senderPublicKey: senderPublicKey)
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
do {
|
||||
let result = try step(ratchet)
|
||||
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
|
||||
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
|
||||
return result
|
||||
} catch {
|
||||
print("[Loki] Couldn't step ratchet due to error: \(error).")
|
||||
|
@ -93,12 +135,12 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
private static func stepRatchetOnceAsync(for groupID: String, senderPublicKey: String) -> Promise<Ratchet> {
|
||||
private static func stepRatchetOnceAsync(for groupPublicKey: String, senderPublicKey: String) -> Promise<Ratchet> {
|
||||
let (promise, seal) = Promise<Ratchet>.pending()
|
||||
LokiAPI.workQueue.async {
|
||||
SnodeAPI.workQueue.async {
|
||||
try! Storage.writeSync { transaction in
|
||||
do {
|
||||
let result = try stepRatchetOnce(for: groupID, senderPublicKey: senderPublicKey, transaction: transaction)
|
||||
let result = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, transaction: transaction)
|
||||
seal.fulfill(result)
|
||||
} catch {
|
||||
seal.reject(error)
|
||||
|
@ -109,28 +151,23 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
private static func stepRatchet(for groupID: String, senderPublicKey: String, until targetKeyIndex: UInt, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
|
||||
private static func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, transaction: YapDatabaseReadWriteTransaction) throws -> Ratchet {
|
||||
#if DEBUG
|
||||
assert(!Thread.isMainThread)
|
||||
#endif
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey) else {
|
||||
let error = RatchetingError.loadingFailed(groupID: groupID, senderPublicKey: senderPublicKey)
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
if targetKeyIndex < ratchet.keyIndex {
|
||||
// In the case where this function is invoked for an old key index, just remove the key generated
|
||||
// earlier from the database. There's no need to advance the ratchet.
|
||||
guard let messageKey = (ratchet.messageKeys.count > targetKeyIndex) ? ratchet.messageKeys[Int(targetKeyIndex)] : nil else {
|
||||
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupID: groupID, senderPublicKey: senderPublicKey)
|
||||
// There's no need to advance the ratchet if this is invoked for an old key index
|
||||
guard ratchet.messageKeys.count > targetKeyIndex else {
|
||||
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
var messageKeysCopy = ratchet.messageKeys
|
||||
messageKeysCopy.remove(at: Int(targetKeyIndex))
|
||||
let result = Ratchet(chainKey: ratchet.chainKey, keyIndex: ratchet.keyIndex, messageKeys: messageKeysCopy)
|
||||
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
|
||||
return result
|
||||
return ratchet
|
||||
} else {
|
||||
var currentKeyIndex = ratchet.keyIndex
|
||||
var result = ratchet
|
||||
|
@ -142,17 +179,17 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
throw error
|
||||
}
|
||||
}
|
||||
Storage.setClosedGroupRatchet(groupID: groupID, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
|
||||
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, transaction: transaction)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private static func stepRatchetAsync(for groupID: String, senderPublicKey: String, until targetKeyIndex: UInt) -> Promise<Ratchet> {
|
||||
private static func stepRatchetAsync(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt) -> Promise<Ratchet> {
|
||||
let (promise, seal) = Promise<Ratchet>.pending()
|
||||
LokiAPI.workQueue.async {
|
||||
SnodeAPI.workQueue.async {
|
||||
try! Storage.writeSync { transaction in
|
||||
do {
|
||||
let result = try stepRatchet(for: groupID, senderPublicKey: senderPublicKey, until: targetKeyIndex, transaction: transaction)
|
||||
let result = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: targetKeyIndex, transaction: transaction)
|
||||
seal.fulfill(result)
|
||||
} catch {
|
||||
seal.reject(error)
|
||||
|
@ -162,8 +199,30 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
return promise
|
||||
}
|
||||
|
||||
public static func decrypt(_ ivAndCiphertext: Data, for groupID: String, senderPublicKey: String, keyIndex: UInt) -> Promise<Data> {
|
||||
return stepRatchetAsync(for: groupID, senderPublicKey: senderPublicKey, until: keyIndex).map2 { ratchet in
|
||||
@objc(encryptPlaintext:forGroupWithPublicKey:senderPublicKey:)
|
||||
static func objc_encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String) -> [Any]? {
|
||||
guard let (ivAndCiphertext, keyIndex) = try? encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey).wait() else { return nil }
|
||||
return [ ivAndCiphertext, NSNumber(value: keyIndex) ]
|
||||
}
|
||||
|
||||
public static func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String) -> Promise<(ivAndCiphertext: Data, keyIndex: UInt)> {
|
||||
return stepRatchetOnceAsync(for: groupPublicKey, senderPublicKey: senderPublicKey).map2 { ratchet in
|
||||
let iv = Data.getSecureRandomData(ofSize: ivSize)!
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
|
||||
let messageKey = ratchet.messageKeys.last!
|
||||
let aes = try AES(key: messageKey.bytes, blockMode: gcm, padding: .noPadding)
|
||||
let ciphertext = try aes.encrypt(plaintext.bytes)
|
||||
return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(decryptCiphertext:forGroupWithPublicKey:senderPublicKey:keyIndex:)
|
||||
static func objc_decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt) -> Data? {
|
||||
return try? decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex).wait()
|
||||
}
|
||||
|
||||
public static func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt) -> Promise<Data> {
|
||||
return stepRatchetAsync(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex).map2 { ratchet in
|
||||
let iv = ivAndCiphertext[0..<Int(ivSize)]
|
||||
let ciphertext = ivAndCiphertext[Int(ivSize)...]
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
|
||||
|
@ -173,14 +232,40 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
public static func encrypt(_ plaintext: Data, for groupID: String, senderPublicKey: String) -> Promise<Data> {
|
||||
return stepRatchetOnceAsync(for: groupID, senderPublicKey: senderPublicKey).map2 { ratchet in
|
||||
let iv = Data.getSecureRandomData(ofSize: ivSize)!
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
|
||||
let messageKey = ratchet.messageKeys.last!
|
||||
let aes = try AES(key: messageKey.bytes, blockMode: gcm, padding: .noPadding)
|
||||
let ciphertext = try aes.encrypt(plaintext.bytes)
|
||||
return iv + Data(bytes: ciphertext)
|
||||
@objc(handleSharedSenderKeysUpdateIfNeeded:transaction:)
|
||||
public static func handleSharedSenderKeysUpdateIfNeeded(_ dataMessage: SSKProtoDataMessage, using transaction: YapDatabaseReadWriteTransaction) -> Bool {
|
||||
guard let closedGroupUpdate = dataMessage.closedGroupUpdate else { return false }
|
||||
switch closedGroupUpdate.type {
|
||||
case .new:
|
||||
// Unwrap the message
|
||||
let groupPublicKey = closedGroupUpdate.groupPublicKey
|
||||
let name = closedGroupUpdate.name
|
||||
let groupPrivateKey = closedGroupUpdate.groupPrivateKey!
|
||||
let chainKeys = closedGroupUpdate.chainKeys
|
||||
let members = closedGroupUpdate.members
|
||||
let admins = closedGroupUpdate.admins
|
||||
// Persist the ratchets
|
||||
zip(members, chainKeys).forEach { (member, chainKey) in
|
||||
let ratchet = Ratchet(chainKey: chainKey.toHexString(), keyIndex: 0, messageKeys: [])
|
||||
Storage.setClosedGroupRatchet(groupPublicKey: groupPublicKey.toHexString(), senderPublicKey: member, ratchet: ratchet, transaction: transaction)
|
||||
}
|
||||
// Create the group
|
||||
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey.toHexString())
|
||||
let group = TSGroupModel(title: name, memberIds: members, image: nil, groupId: groupID, groupType: .closedGroup, adminIds: admins)
|
||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||
thread.usesSharedSenderKeys = true
|
||||
thread.save(with: transaction)
|
||||
SSKEnvironment.shared.profileManager.addThread(toProfileWhitelist: thread)
|
||||
// Add the group to the user's set of public keys to poll for
|
||||
let groupKeyPair = ECKeyPair(publicKey: groupPublicKey, privateKey: groupPrivateKey)!
|
||||
Storage.addClosedGroupKeyPair(groupKeyPair)
|
||||
// Notify the user
|
||||
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeGroupUpdate)
|
||||
infoMessage.save(with: transaction)
|
||||
// Establish sessions if needed
|
||||
establishSessionsIfNeeded(with: members, in: thread, using: transaction)
|
||||
// Return
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -188,10 +273,10 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
public static func shouldIgnoreClosedGroupMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSThread, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
|
||||
guard let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup,
|
||||
dataMessage.group?.type == .deliver else { return false }
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
var result = false
|
||||
Storage.read { transaction in
|
||||
result = !thread.isUser(inGroup: sender, transaction: transaction)
|
||||
result = !thread.isUser(inGroup: publicKey, transaction: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
@ -199,16 +284,17 @@ public final class ClosedGroupsProtocol : NSObject {
|
|||
@objc(shouldIgnoreClosedGroupUpdateMessage:inThread:wrappedIn:)
|
||||
public static func shouldIgnoreClosedGroupUpdateMessage(_ dataMessage: SSKProtoDataMessage, in thread: TSGroupThread?, wrappedIn envelope: SSKProtoEnvelope) -> Bool {
|
||||
guard let thread = thread else { return false }
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
var result = false
|
||||
Storage.read { transaction in
|
||||
result = !thread.isUserAdmin(inGroup: sender, transaction: transaction)
|
||||
result = !thread.isUserAdmin(inGroup: publicKey, transaction: transaction)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(establishSessionsIfNeededWithClosedGroupMembers:inThread:transaction:)
|
||||
public static func establishSessionsIfNeeded(with closedGroupMembers: [String], in thread: TSGroupThread, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
guard thread.groupModel.groupType == .closedGroup else { return }
|
||||
closedGroupMembers.forEach { member in
|
||||
SessionManagementProtocol.establishSessionIfNeeded(with: member, using: transaction)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
internal extension Storage {
|
||||
|
||||
// MARK: Ratchets
|
||||
internal static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
|
||||
|
||||
internal static func getClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String) -> ClosedGroupsProtocol.Ratchet? {
|
||||
let key = "\(groupPublicKey).\(senderPublicKey)"
|
||||
var result: ClosedGroupsProtocol.Ratchet?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: key, inCollection: closedGroupRatchetCollection) as? ClosedGroupsProtocol.Ratchet
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func setClosedGroupRatchet(groupPublicKey: String, senderPublicKey: String, ratchet: ClosedGroupsProtocol.Ratchet, transaction: YapDatabaseReadWriteTransaction) {
|
||||
let key = "\(groupPublicKey).\(senderPublicKey)"
|
||||
transaction.setObject(ratchet, forKey: key, inCollection: closedGroupRatchetCollection)
|
||||
}
|
||||
}
|
||||
|
||||
@objc internal extension Storage {
|
||||
|
||||
// MARK: Key Pairs
|
||||
internal static let closedGroupKeyPairCollection = "LokiClosedGroupKeyPairCollection"
|
||||
|
||||
internal static func getUserClosedGroupPublicKeys() -> Set<String> {
|
||||
var result: Set<String> = []
|
||||
read { transaction in
|
||||
result = Set(transaction.allKeys(inCollection: closedGroupKeyPairCollection))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(getKeyPairForClosedGroupWithPublicKey:)
|
||||
internal static func getClosedGroupKeyPair(for publicKey: String) -> ECKeyPair? {
|
||||
var result: ECKeyPair?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: publicKey, inCollection: closedGroupKeyPairCollection) as? ECKeyPair
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
internal static func addClosedGroupKeyPair(_ keyPair: ECKeyPair) {
|
||||
try! writeSync { transaction in
|
||||
transaction.setObject(keyPair, forKey: keyPair.hexEncodedPublicKey, inCollection: closedGroupKeyPairCollection)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
|
||||
public extension Storage {
|
||||
|
||||
public static let closedGroupRatchetCollection = "LokiClosedGroupRatchetCollection"
|
||||
|
||||
public static func getClosedGroupRatchet(groupID: String, senderPublicKey: String) -> ClosedGroupsProtocol.Ratchet? {
|
||||
let collection = closedGroupRatchetCollection
|
||||
let key = "\(groupID).\(senderPublicKey)"
|
||||
var result: ClosedGroupsProtocol.Ratchet?
|
||||
read { transaction in
|
||||
result = transaction.object(forKey: key, inCollection: collection) as? ClosedGroupsProtocol.Ratchet
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public static func setClosedGroupRatchet(groupID: String, senderPublicKey: String, ratchet: ClosedGroupsProtocol.Ratchet, transaction: YapDatabaseReadWriteTransaction) {
|
||||
let collection = closedGroupRatchetCollection
|
||||
let key = "\(groupID).\(senderPublicKey)"
|
||||
transaction.setObject(ratchet, forKey: key, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -36,9 +36,14 @@ public final class SessionMetaProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
result = Set(outgoingGroupMessage.sendingRecipientIds())
|
||||
.intersection(thread.groupModel.groupMemberIds)
|
||||
.subtracting(MultiDeviceProtocol.getUserLinkedDevices())
|
||||
if let groupThread = thread as? TSGroupThread, groupThread.usesSharedSenderKeys {
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupThread.groupModel.groupId)
|
||||
result = [ groupPublicKey ]
|
||||
} else {
|
||||
result = Set(outgoingGroupMessage.sendingRecipientIds())
|
||||
.intersection(thread.groupModel.groupMemberIds)
|
||||
.subtracting(MultiDeviceProtocol.getUserLinkedDevices())
|
||||
}
|
||||
}
|
||||
return NSMutableSet(set: result)
|
||||
}
|
||||
|
@ -72,23 +77,23 @@ public final class SessionMetaProtocol : NSObject {
|
|||
// MARK: Typing Indicators
|
||||
/// Invoked only if typing indicators are enabled in the settings. Provides an opportunity
|
||||
/// to avoid sending them if certain conditions are met.
|
||||
@objc(shouldSendTypingIndicatorForThread:)
|
||||
public static func shouldSendTypingIndicator(for thread: TSThread) -> Bool {
|
||||
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
|
||||
@objc(shouldSendTypingIndicatorInThread:)
|
||||
public static func shouldSendTypingIndicator(in thread: TSThread) -> Bool {
|
||||
guard !thread.isGroupThread(), let publicKey = thread.contactIdentifier() else { return false }
|
||||
var isContactFriend = false
|
||||
storage.dbReadConnection.read { transaction in
|
||||
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
|
||||
isContactFriend = (storage.getFriendRequestStatus(for: publicKey, transaction: transaction) == .friends)
|
||||
}
|
||||
return isContactFriend
|
||||
}
|
||||
|
||||
// MARK: Receipts
|
||||
@objc(shouldSendReceiptForThread:)
|
||||
public static func shouldSendReceipt(for thread: TSThread) -> Bool {
|
||||
guard !thread.isGroupThread(), let contactID = thread.contactIdentifier() else { return false }
|
||||
@objc(shouldSendReceiptInThread:)
|
||||
public static func shouldSendReceipt(in thread: TSThread) -> Bool {
|
||||
guard !thread.isGroupThread(), let publicKey = thread.contactIdentifier() else { return false }
|
||||
var isContactFriend = false
|
||||
storage.dbReadConnection.read { transaction in
|
||||
isContactFriend = (storage.getFriendRequestStatus(for: contactID, transaction: transaction) == .friends)
|
||||
isContactFriend = (storage.getFriendRequestStatus(for: publicKey, transaction: transaction) == .friends)
|
||||
}
|
||||
return isContactFriend
|
||||
}
|
||||
|
@ -102,7 +107,7 @@ public final class SessionMetaProtocol : NSObject {
|
|||
return result.source == getUserHexEncodedPublicKey() // Should never occur
|
||||
}
|
||||
|
||||
@objc(updateDisplayNameIfNeededForHexEncodedPublicKey:using:transaction:)
|
||||
@objc(updateDisplayNameIfNeededForPublicKey:using:transaction:)
|
||||
public static func updateDisplayNameIfNeeded(for publicKey: String, using dataMessage: SSKProtoDataMessage, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
guard let profile = dataMessage.profile, let rawDisplayName = profile.displayName, !rawDisplayName.isEmpty else { return }
|
||||
let shortID = publicKey.substring(from: publicKey.index(publicKey.endIndex, offsetBy: -8))
|
||||
|
|
|
@ -24,7 +24,7 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
|
||||
// MARK: Multi Device Destination
|
||||
public struct MultiDeviceDestination : Hashable {
|
||||
public let hexEncodedPublicKey: String
|
||||
public let publicKey: String
|
||||
public let isMaster: Bool
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,7 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
private static func copy(_ messageSend: OWSMessageSend, for destination: MultiDeviceDestination, with seal: Resolver<Void>) -> OWSMessageSend {
|
||||
var recipient: SignalRecipient!
|
||||
storage.dbReadConnection.read { transaction in
|
||||
recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: destination.hexEncodedPublicKey, transaction: transaction)
|
||||
recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: destination.publicKey, transaction: transaction)
|
||||
}
|
||||
// TODO: Why is it okay that the thread, sender certificate, etc. don't get changed?
|
||||
return OWSMessageSend(message: messageSend.message, thread: messageSend.thread, recipient: recipient,
|
||||
|
@ -78,11 +78,11 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
let (threadPromise, threadPromiseSeal) = Promise<TSThread>.pending()
|
||||
if messageSend.message.thread.isGroupThread() {
|
||||
threadPromiseSeal.fulfill(messageSend.message.thread)
|
||||
} else if let thread = TSContactThread.getWithContactId(destination.hexEncodedPublicKey, transaction: transaction) {
|
||||
} else if let thread = TSContactThread.getWithContactId(destination.publicKey, transaction: transaction) {
|
||||
threadPromiseSeal.fulfill(thread)
|
||||
} else {
|
||||
Storage.write { transaction in
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: destination.hexEncodedPublicKey, transaction: transaction)
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: destination.publicKey, transaction: transaction)
|
||||
threadPromiseSeal.fulfill(thread)
|
||||
}
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
}
|
||||
} else {
|
||||
Storage.write { transaction in
|
||||
getAutoGeneratedMultiDeviceFRMessageSend(for: destination.hexEncodedPublicKey, in: transaction, seal: seal)
|
||||
getAutoGeneratedMultiDeviceFRMessageSend(for: destination.publicKey, in: transaction, seal: seal)
|
||||
.done(on: OWSDispatch.sendingQueue()) { autoGeneratedFRMessageSend in
|
||||
messageSender.sendMessage(autoGeneratedFRMessageSend)
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
}
|
||||
|
||||
/// See [Multi Device Message Sending](https://github.com/loki-project/session-protocol-docs/wiki/Multi-Device-Message-Sending) for more information.
|
||||
@objc(sendMessageToDestinationAndLinkedDevices:in:)
|
||||
@objc(sendMessageToDestinationAndLinkedDevices:transaction:)
|
||||
public static func sendMessageToDestinationAndLinkedDevices(_ messageSend: OWSMessageSend, in transaction: YapDatabaseReadTransaction) {
|
||||
if !messageSend.isUDSend && messageSend.recipient.recipientId() != getUserHexEncodedPublicKey() {
|
||||
#if DEBUG
|
||||
|
@ -128,8 +128,8 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
return
|
||||
}
|
||||
print("[Loki] Sending \(type(of: message)) message using multi device routing.")
|
||||
let recipientID = messageSend.recipient.recipientId()
|
||||
getMultiDeviceDestinations(for: recipientID, in: transaction).done2 { destinations in
|
||||
let publicKey = messageSend.recipient.recipientId()
|
||||
getMultiDeviceDestinations(for: publicKey, in: transaction).done2 { destinations in
|
||||
var promises: [Promise<Void>] = []
|
||||
let masterDestination = destinations.first { $0.isMaster }
|
||||
if let masterDestination = masterDestination {
|
||||
|
@ -167,32 +167,25 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
}
|
||||
|
||||
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
|
||||
@objc(getAutoGeneratedMultiDeviceFRMessageForHexEncodedPublicKey:in:)
|
||||
public static func getAutoGeneratedMultiDeviceFRMessage(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> FriendRequestMessage {
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
|
||||
public static func getAutoGeneratedMultiDeviceFRMessage(for publicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> FriendRequestMessage {
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
|
||||
return FriendRequestMessage(timestamp: NSDate.ows_millisecondTimeStamp(), thread: thread, body: "Please accept to enable messages to be synced across devices")
|
||||
}
|
||||
|
||||
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
|
||||
@objc(getAutoGeneratedMultiDeviceFRMessageSendForHexEncodedPublicKey:in:)
|
||||
public static func objc_getAutoGeneratedMultiDeviceFRMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction) -> AnyPromise {
|
||||
return AnyPromise.from(getAutoGeneratedMultiDeviceFRMessageSend(for: hexEncodedPublicKey, in: transaction))
|
||||
}
|
||||
|
||||
/// See [Auto-Generated Friend Requests](https://github.com/loki-project/session-protocol-docs/wiki/Auto-Generated-Friend-Requests) for more information.
|
||||
public static func getAutoGeneratedMultiDeviceFRMessageSend(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadWriteTransaction, seal externalSeal: Resolver<Void>? = nil) -> Promise<OWSMessageSend> {
|
||||
public static func getAutoGeneratedMultiDeviceFRMessageSend(for publicKey: String, in transaction: YapDatabaseReadWriteTransaction, seal externalSeal: Resolver<Void>? = nil) -> Promise<OWSMessageSend> {
|
||||
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: hexEncodedPublicKey, transaction: transaction)
|
||||
let message = getAutoGeneratedMultiDeviceFRMessage(for: hexEncodedPublicKey, in: transaction)
|
||||
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: hexEncodedPublicKey, transaction: transaction)
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
|
||||
let message = getAutoGeneratedMultiDeviceFRMessage(for: publicKey, in: transaction)
|
||||
let recipient = SignalRecipient.getOrBuildUnsavedRecipient(forRecipientId: publicKey, transaction: transaction)
|
||||
let udManager = SSKEnvironment.shared.udManager
|
||||
let senderCertificate = udManager.getSenderCertificate()
|
||||
SSKEnvironment.shared.profileManager.ensureProfileCachedForContact(withID: hexEncodedPublicKey, with: transaction) // Prevent the line below from starting a write transaction
|
||||
SSKEnvironment.shared.profileManager.ensureProfileCachedForContact(withID: publicKey, with: transaction) // Prevent the line below from starting a write transaction
|
||||
let (promise, seal) = Promise<OWSMessageSend>.pending()
|
||||
LokiAPI.workQueue.async {
|
||||
SnodeAPI.workQueue.async {
|
||||
var recipientUDAccess: OWSUDAccess?
|
||||
if let senderCertificate = senderCertificate {
|
||||
recipientUDAccess = udManager.udAccess(forRecipientId: hexEncodedPublicKey, requireSyncAccess: true)
|
||||
recipientUDAccess = udManager.udAccess(forRecipientId: publicKey, requireSyncAccess: true)
|
||||
}
|
||||
let messageSend = OWSMessageSend(message: message, thread: thread, recipient: recipient, senderCertificate: senderCertificate,
|
||||
udAccess: recipientUDAccess, localNumber: getUserHexEncodedPublicKey(), success: {
|
||||
|
@ -205,21 +198,19 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
return promise
|
||||
}
|
||||
|
||||
@objc(updateDeviceLinksIfNeededForHexEncodedPublicKey:in:)
|
||||
public static func updateDeviceLinksIfNeeded(for hexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> AnyPromise {
|
||||
let promise = getMultiDeviceDestinations(for: hexEncodedPublicKey, in: transaction)
|
||||
return AnyPromise.from(promise)
|
||||
@objc(updateDeviceLinksIfNeededForPublicKey:transaction:)
|
||||
public static func updateDeviceLinksIfNeeded(for publicKey: String, in transaction: YapDatabaseReadTransaction) -> AnyPromise {
|
||||
return AnyPromise.from(getMultiDeviceDestinations(for: publicKey, in: transaction))
|
||||
}
|
||||
|
||||
// MARK: - Receiving
|
||||
|
||||
@objc(handleDeviceLinkMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(handleDeviceLinkMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleDeviceLinkMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
guard let deviceLinkMessage = protoContent.lokiDeviceLinkMessage, let master = deviceLinkMessage.masterPublicKey,
|
||||
let slave = deviceLinkMessage.slavePublicKey, let slaveSignature = deviceLinkMessage.slaveSignature else {
|
||||
print("[Loki] Received an invalid device link message.")
|
||||
return
|
||||
return print("[Loki] Received an invalid device link message.")
|
||||
}
|
||||
let deviceLinkingSession = DeviceLinkingSession.current
|
||||
if let masterSignature = deviceLinkMessage.masterSignature { // Authorization
|
||||
|
@ -244,29 +235,30 @@ public final class MultiDeviceProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(handleUnlinkDeviceMessage:wrappedIn:using:)
|
||||
@objc(handleUnlinkDeviceMessage:wrappedIn:transaction:)
|
||||
public static func handleUnlinkDeviceMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
// Check that the request was sent by our master device
|
||||
guard let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
|
||||
let wasSentByMasterDevice = (masterPublicKey == sender)
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: userPublicKey, in: transaction) else { return }
|
||||
let wasSentByMasterDevice = (userMasterPublicKey == publicKey)
|
||||
guard wasSentByMasterDevice else { return }
|
||||
// Ignore the request if we don't know about the device link in question
|
||||
let masterDeviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
|
||||
let masterDeviceLinks = storage.getDeviceLinks(for: userMasterPublicKey, in: transaction)
|
||||
if !masterDeviceLinks.contains(where: {
|
||||
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
|
||||
$0.master.hexEncodedPublicKey == userMasterPublicKey && $0.slave.hexEncodedPublicKey == userPublicKey
|
||||
}) {
|
||||
return
|
||||
}
|
||||
LokiFileServerAPI.getDeviceLinks(associatedWith: getUserHexEncodedPublicKey()).done2 { slaveDeviceLinks in
|
||||
FileServerAPI.getDeviceLinks(associatedWith: userPublicKey).done2 { slaveDeviceLinks in
|
||||
// Check that the device link IS present on the file server.
|
||||
// Note that the device link as seen from the master device's perspective has been deleted at this point, but the
|
||||
// device link as seen from the slave perspective hasn't.
|
||||
if slaveDeviceLinks.contains(where: {
|
||||
$0.master.hexEncodedPublicKey == masterPublicKey && $0.slave.hexEncodedPublicKey == getUserHexEncodedPublicKey()
|
||||
$0.master.hexEncodedPublicKey == userMasterPublicKey && $0.slave.hexEncodedPublicKey == userPublicKey
|
||||
}) {
|
||||
for deviceLink in slaveDeviceLinks { // In theory there should only be one
|
||||
LokiFileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
|
||||
FileServerAPI.removeDeviceLink(deviceLink) // Attempt to clean up on the file server
|
||||
}
|
||||
UserDefaults.standard[.wasUnlinked] = true
|
||||
DispatchQueue.main.async {
|
||||
|
@ -288,10 +280,10 @@ public extension MultiDeviceProtocol {
|
|||
storage.dbReadConnection.read { transaction in
|
||||
var destinations: Set<MultiDeviceDestination> = []
|
||||
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
|
||||
let masterDestination = MultiDeviceDestination(hexEncodedPublicKey: masterPublicKey, isMaster: true)
|
||||
let masterDestination = MultiDeviceDestination(publicKey: masterPublicKey, isMaster: true)
|
||||
destinations.insert(masterDestination)
|
||||
let deviceLinks = storage.getDeviceLinks(for: masterPublicKey, in: transaction)
|
||||
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(hexEncodedPublicKey: $0.slave.hexEncodedPublicKey, isMaster: false) }
|
||||
let slaveDestinations = deviceLinks.map { MultiDeviceDestination(publicKey: $0.slave.hexEncodedPublicKey, isMaster: false) }
|
||||
destinations.formUnion(slaveDestinations)
|
||||
seal.fulfill(destinations)
|
||||
}
|
||||
|
@ -304,11 +296,11 @@ public extension MultiDeviceProtocol {
|
|||
}
|
||||
if timeSinceLastUpdate > deviceLinkUpdateInterval {
|
||||
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
|
||||
LokiFileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
|
||||
FileServerAPI.getDeviceLinks(associatedWith: masterPublicKey).done2 { _ in
|
||||
getDestinations()
|
||||
lastDeviceLinkUpdate[publicKey] = Date()
|
||||
}.catch2 { error in
|
||||
if (error as? LokiDotNetAPI.LokiDotNetAPIError) == LokiDotNetAPI.LokiDotNetAPIError.parsingFailed {
|
||||
if (error as? DotNetAPI.DotNetAPIError) == DotNetAPI.DotNetAPIError.parsingFailed {
|
||||
// Don't immediately re-fetch in case of failure due to a parsing error
|
||||
lastDeviceLinkUpdate[publicKey] = Date()
|
||||
getDestinations()
|
||||
|
|
|
@ -63,6 +63,7 @@ public final class SessionManagementProtocol : NSObject {
|
|||
if message is FriendRequestMessage { return false }
|
||||
else if message is SessionRequestMessage { return false }
|
||||
else if let message = message as? DeviceLinkMessage, message.kind == .request { return false }
|
||||
else if (message.thread as? TSGroupThread)?.usesSharedSenderKeys == true { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -110,7 +111,7 @@ public final class SessionManagementProtocol : NSObject {
|
|||
messageSenderJobQueue.add(message: sessionRequestMessage, transaction: transaction)
|
||||
}
|
||||
|
||||
@objc(sendSessionEstablishedMessageToPublicKey:in:)
|
||||
@objc(sendSessionEstablishedMessageToPublicKey:transaction:)
|
||||
public static func sendSessionEstablishedMessage(to publicKey: String, in transaction: YapDatabaseReadWriteTransaction) {
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
|
||||
thread.save(with: transaction)
|
||||
|
@ -119,6 +120,9 @@ public final class SessionManagementProtocol : NSObject {
|
|||
messageSenderJobQueue.add(message: ephemeralMessage, transaction: transaction)
|
||||
}
|
||||
|
||||
/// DEPRECATED.
|
||||
///
|
||||
/// Only relevant for closed groups that don't use shared sender keys.
|
||||
@objc(shouldIgnoreMissingPreKeyBundleExceptionForMessage:to:)
|
||||
public static func shouldIgnoreMissingPreKeyBundleException(for message: TSOutgoingMessage, to hexEncodedPublicKey: String) -> Bool {
|
||||
// When a closed group is created, members try to establish sessions with eachother in the background through
|
||||
|
@ -126,15 +130,15 @@ public final class SessionManagementProtocol : NSObject {
|
|||
// bundles contained in the session requests and replied with background messages to finalize the session
|
||||
// creation, a given user won't be able to successfully send a message to all members of a group. This check
|
||||
// is so that until we can do better on this front the user at least won't see this as an error in the UI.
|
||||
return (message.thread as? TSGroupThread)?.groupModel.groupType == .closedGroup
|
||||
guard let groupThread = message.thread as? TSGroupThread else { return false }
|
||||
return groupThread.groupModel.groupType == .closedGroup && !groupThread.usesSharedSenderKeys
|
||||
}
|
||||
|
||||
@objc(startSessionResetInThread:using:)
|
||||
@objc(startSessionResetInThread:transaction:)
|
||||
public static func startSessionReset(in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
// Check preconditions
|
||||
guard let thread = thread as? TSContactThread else {
|
||||
print("[Loki] Can't restore session for non contact thread.")
|
||||
return
|
||||
return print("[Loki] Can't restore session for non contact thread.")
|
||||
}
|
||||
// Send session restoration request messages to the devices requiring session restoration
|
||||
let devices = thread.sessionRestoreDevices // TODO: Rename this to something that reads better
|
||||
|
@ -157,7 +161,7 @@ public final class SessionManagementProtocol : NSObject {
|
|||
|
||||
// MARK: - Receiving
|
||||
|
||||
@objc(handleDecryptionError:forPublicKey:using:)
|
||||
@objc(handleDecryptionError:forPublicKey:transaction:)
|
||||
public static func handleDecryptionError(_ rawValue: Int32, for publicKey: String, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let type = TSErrorMessageType(rawValue: rawValue)
|
||||
let masterPublicKey = storage.getMasterHexEncodedPublicKey(for: publicKey, in: transaction) ?? publicKey
|
||||
|
@ -171,47 +175,47 @@ public final class SessionManagementProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(handlePreKeyBundleMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handlePreKeyBundleMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
guard let preKeyBundleMessage = protoContent.prekeyBundleMessage else { return }
|
||||
print("[Loki] Received a pre key bundle message from: \(sender).")
|
||||
print("[Loki] Received a pre key bundle message from: \(publicKey).")
|
||||
guard let preKeyBundle = preKeyBundleMessage.getPreKeyBundle(with: transaction) else {
|
||||
print("[Loki] Couldn't parse pre key bundle received from: \(sender).")
|
||||
return
|
||||
return print("[Loki] Couldn't parse pre key bundle received from: \(publicKey).")
|
||||
}
|
||||
if isSessionRequestMessage(protoContent.dataMessage),
|
||||
let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: sender, in: transaction),
|
||||
let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: publicKey, in: transaction),
|
||||
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
|
||||
// We sent a session request after this one was sent
|
||||
print("[Loki] Ignoring session request from: \(sender).")
|
||||
return
|
||||
return print("[Loki] Ignoring session request from: \(publicKey).")
|
||||
}
|
||||
storage.setPreKeyBundle(preKeyBundle, forContact: sender, transaction: transaction)
|
||||
storage.setPreKeyBundle(preKeyBundle, forContact: publicKey, transaction: transaction)
|
||||
}
|
||||
|
||||
/// - Note: Must be invoked after `handlePreKeyBundleMessageIfNeeded(_:wrappedIn:using:)`.
|
||||
@objc(handleSessionRequestMessage:wrappedIn:using:)
|
||||
public static func handleSessionRequestMessage(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: sender, in: transaction),
|
||||
@objc(handleSessionRequestMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleSessionRequestMessageIfNeeded(_ protoContent: SSKProtoContent, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) -> Bool {
|
||||
guard isSessionRequestMessage(protoContent.dataMessage) else { return false }
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
if let sentSessionRequestTimestamp = storage.getSessionRequestTimestamp(for: publicKey, in: transaction),
|
||||
envelope.timestamp < NSDate.ows_millisecondsSince1970(for: sentSessionRequestTimestamp) {
|
||||
// We sent a session request after this one was sent
|
||||
print("[Loki] Ignoring session request from: \(sender).")
|
||||
return
|
||||
print("[Loki] Ignoring session request from: \(publicKey).")
|
||||
return false
|
||||
}
|
||||
sendSessionEstablishedMessage(to: sender, in: transaction)
|
||||
sendSessionEstablishedMessage(to: publicKey, in: transaction)
|
||||
return true
|
||||
}
|
||||
|
||||
@objc(handleEndSessionMessageReceivedInThread:using:)
|
||||
public static func handleEndSessionMessageReceived(in thread: TSContactThread, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let sender = thread.contactIdentifier()
|
||||
print("[Loki] End session message received from: \(sender).")
|
||||
let publicKey = thread.contactIdentifier()
|
||||
print("[Loki] End session message received from: \(publicKey).")
|
||||
// Notify the user
|
||||
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageType: .typeLokiSessionResetInProgress)
|
||||
infoMessage.save(with: transaction)
|
||||
// Archive all sessions
|
||||
storage.archiveAllSessions(forContact: sender, protocolContext: transaction)
|
||||
storage.archiveAllSessions(forContact: publicKey, protocolContext: transaction)
|
||||
// Update the session reset status
|
||||
thread.sessionResetStatus = .requestReceived
|
||||
thread.save(with: transaction)
|
||||
|
|
|
@ -22,10 +22,10 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
@objc public static func syncProfile() {
|
||||
try! Storage.writeSync { transaction in
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
|
||||
for publicKey in linkedDevices {
|
||||
guard publicKey != userPublicKey else { continue }
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: publicKey, transaction: transaction)
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
|
||||
for device in userLinkedDevices {
|
||||
guard device != userPublicKey else { continue }
|
||||
let thread = TSContactThread.getOrCreateThread(withContactId: device, transaction: transaction)
|
||||
thread.save(with: transaction)
|
||||
let syncMessage = OWSOutgoingSyncMessage.init(in: thread, messageBody: "", attachmentId: nil)
|
||||
syncMessage.save(with: transaction)
|
||||
|
@ -60,14 +60,14 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
}
|
||||
|
||||
@objc public static func syncAllClosedGroups() -> AnyPromise {
|
||||
var groups: [TSGroupThread] = []
|
||||
var closedGroups: [TSGroupThread] = []
|
||||
TSGroupThread.enumerateCollectionObjects { object, _ in
|
||||
guard let group = object as? TSGroupThread, group.groupModel.groupType == .closedGroup,
|
||||
group.shouldThreadBeVisible else { return }
|
||||
groups.append(group)
|
||||
guard let closedGroup = object as? TSGroupThread, closedGroup.groupModel.groupType == .closedGroup,
|
||||
closedGroup.shouldThreadBeVisible else { return }
|
||||
closedGroups.append(closedGroup)
|
||||
}
|
||||
let syncManager = SSKEnvironment.shared.syncManager
|
||||
let promises = groups.map { group -> Promise<Void> in
|
||||
let promises = closedGroups.map { group -> Promise<Void> in
|
||||
return Promise(syncManager.syncGroup(for: group)).map2 { _ in }
|
||||
}
|
||||
return AnyPromise.from(when(fulfilled: promises))
|
||||
|
@ -87,8 +87,8 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
|
||||
// MARK: - Receiving
|
||||
|
||||
@objc(isValidSyncMessage:in:)
|
||||
public static func isValidSyncMessage(_ envelope: SSKProtoEnvelope, in transaction: YapDatabaseReadTransaction) -> Bool {
|
||||
@objc(isValidSyncMessage:transaction:)
|
||||
public static func isValidSyncMessage(_ envelope: SSKProtoEnvelope, transaction: YapDatabaseReadTransaction) -> Bool {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
return LokiDatabaseUtilities.isUserLinkedDevice(publicKey, transaction: transaction)
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
syncMessageTimestamps[publicKey] = timestamps
|
||||
}
|
||||
|
||||
@objc(isDuplicateSyncMessage:fromHexEncodedPublicKey:)
|
||||
@objc(isDuplicateSyncMessage:fromPublicKey:)
|
||||
public static func isDuplicateSyncMessage(_ protoContent: SSKProtoContent, from publicKey: String) -> Bool {
|
||||
guard let syncMessage = protoContent.syncMessage?.sent else { return false }
|
||||
var timestamps: Set<UInt64> = syncMessageTimestamps[publicKey] ?? []
|
||||
|
@ -111,41 +111,47 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
return result
|
||||
}
|
||||
|
||||
@objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(updateProfileFromSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func updateProfileFromSyncMessageIfNeeded(_ dataMessage: SSKProtoDataMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let sender = envelope.source! // Set during UD decryption
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
guard let userMasterPublicKey = storage.getMasterHexEncodedPublicKey(for: getUserHexEncodedPublicKey(), in: transaction) else { return }
|
||||
let wasSentByMasterDevice = (userMasterPublicKey == sender)
|
||||
let wasSentByMasterDevice = (userMasterPublicKey == publicKey)
|
||||
guard wasSentByMasterDevice else { return }
|
||||
SessionMetaProtocol.updateDisplayNameIfNeeded(for: userMasterPublicKey, using: dataMessage, in: transaction)
|
||||
SessionMetaProtocol.updateProfileKeyIfNeeded(for: userMasterPublicKey, using: dataMessage)
|
||||
}
|
||||
|
||||
@objc(handleClosedGroupUpdatedSyncMessageIfNeeded:using:)
|
||||
public static func handleClosedGroupUpdatedSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
@objc(handleClosedGroupUpdateSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleClosedGroupUpdateSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
// Check preconditions
|
||||
guard let group = transcript.dataMessage.group, let name = group.name else { return }
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
|
||||
guard wasSentByLinkedDevice, let group = transcript.dataMessage.group, let name = group.name else { return }
|
||||
// Create or update the group
|
||||
let id = group.id
|
||||
let members = group.members
|
||||
let newGroupThread = TSGroupThread.getOrCreateThread(withGroupId: id, groupType: .closedGroup, transaction: transaction)
|
||||
let newGroupModel = TSGroupModel(title: name, memberIds: members, image: nil, groupId: id, groupType: .closedGroup, adminIds: group.admins)
|
||||
newGroupThread.groupModel = newGroupModel // TODO: Should this use the setGroupModel method on TSGroupThread?
|
||||
newGroupThread.save(with: transaction)
|
||||
newGroupThread.setGroupModel(newGroupModel, with: transaction)
|
||||
OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction)
|
||||
// Try to establish sessions with all members for which none exists yet when a group is created or updated
|
||||
ClosedGroupsProtocol.establishSessionsIfNeeded(with: members, in: newGroupThread, using: transaction)
|
||||
OWSDisappearingMessagesJob.shared().becomeConsistent(withDisappearingDuration: transcript.dataMessage.expireTimer, thread: newGroupThread, createdByRemoteRecipientId: nil, createdInExistingGroup: true, transaction: transaction)
|
||||
// Notify the user
|
||||
let contactsManager = SSKEnvironment.shared.contactsManager
|
||||
let groupUpdatedMessageDescription = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager)
|
||||
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: newGroupThread, messageType: .typeGroupUpdate, customMessage: groupUpdatedMessageDescription)
|
||||
let infoMessageText = newGroupThread.groupModel.getInfoStringAboutUpdate(to: newGroupModel, contactsManager: contactsManager)
|
||||
let infoMessage = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: newGroupThread, messageType: .typeGroupUpdate, customMessage: infoMessageText)
|
||||
infoMessage.save(with: transaction)
|
||||
}
|
||||
|
||||
@objc(handleClosedGroupQuitSyncMessageIfNeeded:using:)
|
||||
public static func handleClosedGroupQuitSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
@objc(handleClosedGroupQuitSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleClosedGroupQuitSyncMessageIfNeeded(_ transcript: OWSIncomingSentMessageTranscript, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
// Check preconditions
|
||||
guard let group = transcript.dataMessage.group else { return }
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
|
||||
guard wasSentByLinkedDevice, let group = transcript.dataMessage.group else { return }
|
||||
// Leave the group
|
||||
let groupThread = TSGroupThread.getOrCreateThread(withGroupId: group.id, groupType: .closedGroup, transaction: transaction)
|
||||
groupThread.save(with: transaction)
|
||||
|
@ -155,12 +161,12 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
infoMessage.save(with: transaction)
|
||||
}
|
||||
|
||||
@objc(handleContactSyncMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(handleContactSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleContactSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
|
||||
guard wasSentByLinkedDevice, let contacts = syncMessage.contacts, let contactsAsData = contacts.data, contactsAsData.count > 0 else { return }
|
||||
guard wasSentByLinkedDevice, let contacts = syncMessage.contacts, let contactsAsData = contacts.data, !contactsAsData.isEmpty else { return }
|
||||
print("[Loki] Contact sync message received.")
|
||||
handleContactSyncMessageData(contactsAsData, using: transaction)
|
||||
}
|
||||
|
@ -169,10 +175,10 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
let parser = ContactParser(data: data)
|
||||
let publicKeys = parser.parseHexEncodedPublicKeys()
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: userPublicKey, in: transaction)
|
||||
// Try to establish sessions
|
||||
for publicKey in publicKeys {
|
||||
guard !linkedDevices.contains(publicKey) else { continue } // Skip self and linked devices
|
||||
guard !userLinkedDevices.contains(publicKey) else { continue } // Skip self and linked devices
|
||||
// We don't update the friend request status; that's done in OWSMessageSender.sendMessage(_:)
|
||||
let friendRequestStatus = storage.getFriendRequestStatus(for: publicKey, transaction: transaction)
|
||||
switch friendRequestStatus {
|
||||
|
@ -199,42 +205,43 @@ public final class SyncMessagesProtocol : NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
@objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(handleClosedGroupSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleClosedGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
|
||||
guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, groupsAsData.count > 0 else { return }
|
||||
guard wasSentByLinkedDevice, let groups = syncMessage.groups, let groupsAsData = groups.data, !groupsAsData.isEmpty else { return }
|
||||
print("[Loki] Closed group sync message received.")
|
||||
let parser = ClosedGroupParser(data: groupsAsData)
|
||||
let groupModels = parser.parseGroupModels()
|
||||
for groupModel in groupModels {
|
||||
var thread: TSGroupThread! = TSGroupThread(groupId: groupModel.groupId, transaction: transaction)
|
||||
let closedGroups = parser.parseGroupModels()
|
||||
for closedGroup in closedGroups {
|
||||
var thread: TSGroupThread! = TSGroupThread(groupId: closedGroup.groupId, transaction: transaction)
|
||||
if thread == nil {
|
||||
thread = TSGroupThread.getOrCreateThread(with: groupModel, transaction: transaction)
|
||||
thread = TSGroupThread.getOrCreateThread(with: closedGroup, transaction: transaction)
|
||||
thread.shouldThreadBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
ClosedGroupsProtocol.establishSessionsIfNeeded(with: groupModel.groupMemberIds, in: thread, using: transaction)
|
||||
ClosedGroupsProtocol.establishSessionsIfNeeded(with: closedGroup.groupMemberIds, in: thread, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:using:)
|
||||
@objc(handleOpenGroupSyncMessageIfNeeded:wrappedIn:transaction:)
|
||||
public static func handleOpenGroupSyncMessageIfNeeded(_ syncMessage: SSKProtoSyncMessage, wrappedIn envelope: SSKProtoEnvelope, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
let publicKey = envelope.source! // Set during UD decryption
|
||||
let userLinkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: getUserHexEncodedPublicKey(), in: transaction)
|
||||
let wasSentByLinkedDevice = userLinkedDevices.contains(publicKey)
|
||||
guard wasSentByLinkedDevice else { return }
|
||||
let groups = syncMessage.openGroups
|
||||
guard groups.count > 0 else { return }
|
||||
let openGroups = syncMessage.openGroups
|
||||
guard !openGroups.isEmpty else { return }
|
||||
print("[Loki] Open group sync message received.")
|
||||
for openGroup in groups {
|
||||
let openGroupManager = LokiPublicChatManager.shared
|
||||
let openGroupManager = LokiPublicChatManager.shared
|
||||
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
|
||||
let userDisplayName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey, transaction: transaction)
|
||||
for openGroup in openGroups {
|
||||
guard openGroupManager.getChat(server: openGroup.url, channel: openGroup.channelID) == nil else { return }
|
||||
let userPublicKey = UserDefaults.standard[.masterHexEncodedPublicKey] ?? getUserHexEncodedPublicKey()
|
||||
let displayName = SSKEnvironment.shared.profileManager.profileNameForRecipient(withID: userPublicKey, transaction: transaction)
|
||||
LokiPublicChatAPI.setDisplayName(to: displayName, on: openGroup.url)
|
||||
openGroupManager.addChat(server: openGroup.url, channel: openGroup.channelID)
|
||||
LokiPublicChatAPI.setDisplayName(to: userDisplayName, on: openGroup.url)
|
||||
// TODO: Should we also set the profile picture here?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ public final class LokiPushNotificationManager : NSObject {
|
|||
}
|
||||
|
||||
@objc(acknowledgeDeliveryForMessageWithHash:expiration:hexEncodedPublicKey:)
|
||||
static func acknowledgeDelivery(forMessageWithHash hash: String, expiration: Int, hexEncodedPublicKey: String) {
|
||||
static func acknowledgeDelivery(forMessageWithHash hash: String, expiration: UInt64, hexEncodedPublicKey: String) {
|
||||
let parameters: JSON = [ "lastHash" : hash, "pubKey" : hexEncodedPublicKey, "expiration" : expiration]
|
||||
let url = URL(string: server + "acknowledge_message_delivery")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
|
|
|
@ -3,74 +3,74 @@ import PromiseKit
|
|||
public extension Thenable {
|
||||
|
||||
func then2<U>(_ body: @escaping (T) throws -> U) -> Promise<U.T> where U : Thenable {
|
||||
return then(on: LokiAPI.workQueue, body)
|
||||
return then(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func map2<U>(_ transform: @escaping (T) throws -> U) -> Promise<U> {
|
||||
return map(on: LokiAPI.workQueue, transform)
|
||||
return map(on: SnodeAPI.workQueue, transform)
|
||||
}
|
||||
|
||||
func done2(_ body: @escaping (T) throws -> Void) -> Promise<Void> {
|
||||
return done(on: LokiAPI.workQueue, body)
|
||||
return done(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func get2(_ body: @escaping (T) throws -> Void) -> Promise<T> {
|
||||
return get(on: LokiAPI.workQueue, body)
|
||||
return get(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Thenable where T: Sequence {
|
||||
|
||||
func mapValues2<U>(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> {
|
||||
return mapValues(on: LokiAPI.workQueue, transform)
|
||||
return mapValues(on: SnodeAPI.workQueue, transform)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Guarantee {
|
||||
|
||||
func then2<U>(_ body: @escaping (T) -> Guarantee<U>) -> Guarantee<U> {
|
||||
return then(on: LokiAPI.workQueue, body)
|
||||
return then(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func map2<U>(_ body: @escaping (T) -> U) -> Guarantee<U> {
|
||||
return map(on: LokiAPI.workQueue, body)
|
||||
return map(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func done2(_ body: @escaping (T) -> Void) -> Guarantee<Void> {
|
||||
return done(on: LokiAPI.workQueue, body)
|
||||
return done(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func get2(_ body: @escaping (T) -> Void) -> Guarantee<T> {
|
||||
return get(on: LokiAPI.workQueue, body)
|
||||
return get(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CatchMixin {
|
||||
|
||||
func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer {
|
||||
return self.catch(on: LokiAPI.workQueue, body)
|
||||
return self.catch(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func recover2<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T {
|
||||
return recover(on: LokiAPI.workQueue, body)
|
||||
return recover(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func recover2(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T> {
|
||||
return recover(on: LokiAPI.workQueue, body)
|
||||
return recover(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func ensure2(_ body: @escaping () -> Void) -> Promise<T> {
|
||||
return ensure(on: LokiAPI.workQueue, body)
|
||||
return ensure(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
}
|
||||
|
||||
public extension CatchMixin where T == Void {
|
||||
|
||||
func recover2(_ body: @escaping(Error) -> Void) -> Guarantee<Void> {
|
||||
return recover(on: LokiAPI.workQueue, body)
|
||||
return recover(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
|
||||
func recover2(_ body: @escaping(Error) throws -> Void) -> Promise<Void> {
|
||||
return recover(on: LokiAPI.workQueue, body)
|
||||
return recover(on: SnodeAPI.workQueue, body)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSAssertDebug(envelopeData);
|
||||
|
||||
self = [super initWithUniqueId:[NSUUID new].UUIDString];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
@ -259,6 +260,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
|
|||
OWSSingletonAssert();
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
@ -418,6 +420,7 @@ NSString *const OWSMessageContentJobFinderExtensionGroup = @"OWSMessageContentJo
|
|||
AssertOnDispatchQueue(self.serialQueue);
|
||||
|
||||
NSMutableArray<OWSMessageContentJob *> *processedJobs = [NSMutableArray new];
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (OWSMessageContentJob *job in jobs) {
|
||||
|
||||
|
|
|
@ -283,6 +283,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
// Return to avoid double-acknowledging.
|
||||
return;
|
||||
}
|
||||
case SSKProtoEnvelopeTypeClosedGroupCiphertext: // Loki: Fall through
|
||||
case SSKProtoEnvelopeTypeUnidentifiedSender: {
|
||||
[self decryptUnidentifiedSender:envelope
|
||||
successBlock:^(OWSMessageDecryptResult *result, YapDatabaseReadWriteTransaction *transaction) {
|
||||
|
@ -336,9 +337,9 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
|
||||
NSString *recipientId = envelope.source;
|
||||
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
|
||||
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
|
||||
|
||||
NSData *_Nullable plaintextData = [[cipher decryptWithMessage:encryptedData] removePadding];
|
||||
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientPublicKey:recipientId privateKey:identityKeyPair.privateKey];
|
||||
|
||||
NSData *_Nullable plaintextData = [[cipher decrypt:encryptedData] removePadding];
|
||||
if (!plaintextData) {
|
||||
NSString *errorString = [NSString stringWithFormat:@"Failed to decrypt friend request message from: %@.", recipientId];
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptMessage, errorString);
|
||||
|
@ -500,6 +501,11 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
return failureBlock(cipherError);
|
||||
}
|
||||
|
||||
ECKeyPair *keyPair = nil; // Loki: SMKSecretSessionCipher will fall back on the user's key pair if this is nil
|
||||
if (envelope.type == SSKProtoEnvelopeTypeClosedGroupCiphertext) {
|
||||
keyPair = [LKStorage getKeyPairForClosedGroupWithPublicKey:envelope.source];
|
||||
}
|
||||
|
||||
NSError *decryptError;
|
||||
SMKDecryptResult *_Nullable decryptResult =
|
||||
[cipher throwswrapped_decryptMessageWithCertificateValidator:certificateValidator
|
||||
|
@ -507,6 +513,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
timestamp:serverTimestamp
|
||||
localRecipientId:localRecipientId
|
||||
localDeviceId:localDeviceId
|
||||
keyPair:keyPair
|
||||
protocolContext:transaction
|
||||
error:&decryptError];
|
||||
|
||||
|
@ -543,7 +550,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
|
||||
identifiedEnvelope = [identifiedEnvelopeBuilder buildAndReturnError:&identifiedEnvelopeBuilderError];
|
||||
if (identifiedEnvelopeBuilderError) {
|
||||
OWSFailDebug(@"failure identifiedEnvelopeBuilderError: %@", identifiedEnvelopeBuilderError);
|
||||
OWSFailDebug(@"identifiedEnvelopeBuilderError: %@", identifiedEnvelopeBuilderError);
|
||||
}
|
||||
}
|
||||
OWSAssert(underlyingError);
|
||||
|
@ -578,6 +585,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
return;
|
||||
}
|
||||
|
||||
OWSFailDebug(@"%@", underlyingError);
|
||||
failureBlock(underlyingError);
|
||||
return;
|
||||
}
|
||||
|
@ -596,7 +604,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
|
||||
long sourceDeviceId = decryptResult.senderDeviceId;
|
||||
if (sourceDeviceId < 1 || sourceDeviceId > UINT32_MAX) {
|
||||
NSString *errorDescription = @"Invalid UD sender device id.";
|
||||
NSString *errorDescription = @"Invalid UD sender device ID.";
|
||||
OWSFailDebug(@"%@", errorDescription);
|
||||
NSError *error = OWSErrorWithCodeDescription(OWSErrorCodeFailedToDecryptUDMessage, errorDescription);
|
||||
return failureBlock(error);
|
||||
|
@ -670,7 +678,7 @@ NSError *EnsureDecryptError(NSError *_Nullable error, NSString *fallbackErrorDes
|
|||
OWSAssertDebug(errorMessage);
|
||||
if (errorMessage != nil) {
|
||||
[errorMessage saveWithTransaction:transaction];
|
||||
[LKSessionManagementProtocol handleDecryptionError:errorMessage.errorType forPublicKey:envelope.source using:transaction];
|
||||
[LKSessionManagementProtocol handleDecryptionError:errorMessage.errorType forPublicKey:envelope.source transaction:transaction];
|
||||
[self notifyUserForErrorMessage:errorMessage envelope:envelope transaction:transaction];
|
||||
}
|
||||
} error:nil];
|
||||
|
|
|
@ -403,16 +403,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
OWSPrimaryStorage *storage = OWSPrimaryStorage.sharedManager;
|
||||
__block NSSet<NSString *> *linkedDeviceHexEncodedPublicKeys;
|
||||
__block NSSet<NSString *> *senderLinkedDevices;
|
||||
[storage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
linkedDeviceHexEncodedPublicKeys = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:envelope.source in:transaction];
|
||||
senderLinkedDevices = [LKDatabaseUtilities getLinkedDeviceHexEncodedPublicKeysFor:envelope.source in:transaction];
|
||||
}];
|
||||
|
||||
BOOL duplicateEnvelope = NO;
|
||||
for (NSString *hexEncodedPublicKey in linkedDeviceHexEncodedPublicKeys) {
|
||||
for (NSString *publicKey in senderLinkedDevices) {
|
||||
duplicateEnvelope = duplicateEnvelope
|
||||
|| [self.incomingMessageFinder existsMessageWithTimestamp:envelope.timestamp
|
||||
sourceId:hexEncodedPublicKey
|
||||
sourceId:publicKey
|
||||
sourceDeviceId:envelope.sourceDevice
|
||||
transaction:transaction];
|
||||
}
|
||||
|
@ -423,43 +423,65 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
envelope.timestamp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (envelope.content != nil) {
|
||||
|
||||
// Loki: Decrypt closed group message if applicable
|
||||
NSData *sharedSenderKeysPlaintext = nil;
|
||||
if ([[LKStorage getUserClosedGroupPublicKeys] containsObject:envelope.source]) {
|
||||
NSError *error;
|
||||
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:plaintextData error:&error];
|
||||
if (error || !contentProto) {
|
||||
OWSFailDebug(@"Could not parse proto due to error: %@.", error);
|
||||
SSKProtoClosedGroupCiphertext *_Nullable closedGroupCiphertextProto = [SSKProtoClosedGroupCiphertext parseData:plaintextData error:&error];
|
||||
if (error != nil || closedGroupCiphertextProto == nil) {
|
||||
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
|
||||
return;
|
||||
}
|
||||
NSString *senderPublicKey = closedGroupCiphertextProto.senderPublicKey;
|
||||
uint32_t keyIndex = closedGroupCiphertextProto.keyIndex;
|
||||
sharedSenderKeysPlaintext = [LKClosedGroupsProtocol decryptCiphertext:closedGroupCiphertextProto.ciphertext forGroupWithPublicKey:envelope.source senderPublicKey:senderPublicKey keyIndex:keyIndex];
|
||||
if (sharedSenderKeysPlaintext == nil) {
|
||||
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (envelope.content != nil || sharedSenderKeysPlaintext != nil) {
|
||||
NSError *error;
|
||||
SSKProtoContent *_Nullable contentProto = [SSKProtoContent parseData:(sharedSenderKeysPlaintext ?: plaintextData) error:&error];
|
||||
if (error != nil || contentProto == nil) {
|
||||
OWSFailDebug(@"Couldn't parse proto due to error: %@.", error);
|
||||
return;
|
||||
}
|
||||
OWSLogInfo(@"Handling content: <Content: %@>.", [self descriptionForContent:contentProto]);
|
||||
|
||||
// Loki: Ignore any friend requests from before restoration
|
||||
// Loki: Ignore friend requests from before restoration
|
||||
if ([LKFriendRequestProtocol isFriendRequestFromBeforeRestoration:envelope]) {
|
||||
[LKLogger print:@"[Loki] Ignoring friend request from before restoration."];
|
||||
return;
|
||||
}
|
||||
|
||||
// Loki: Ignore any duplicate sync transcripts
|
||||
if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromHexEncodedPublicKey:envelope.source]) { return; }
|
||||
// Loki: Ignore duplicate sync transcripts
|
||||
if ([LKSyncMessagesProtocol isDuplicateSyncMessage:contentProto fromPublicKey:envelope.source]) {
|
||||
[LKLogger print:@"[Loki] Ignoring duplicate sync transcript."];
|
||||
return;
|
||||
}
|
||||
|
||||
// Loki: Handle pre key bundle message if needed
|
||||
[LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
|
||||
[LKSessionManagementProtocol handlePreKeyBundleMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction];
|
||||
|
||||
// Loki: Handle session request if needed
|
||||
if ([LKSessionManagementProtocol isSessionRequestMessage:contentProto.dataMessage]) {
|
||||
[LKSessionManagementProtocol handleSessionRequestMessage:contentProto.dataMessage wrappedIn:envelope using:transaction];
|
||||
return; // Don't process the message any further
|
||||
if ([LKSessionManagementProtocol handleSessionRequestMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loki: Handle session restoration request if needed
|
||||
if ([LKSessionManagementProtocol isSessionRestorationRequest:contentProto.dataMessage]) { return; } // Don't process the message any further
|
||||
if ([LKSessionManagementProtocol isSessionRestorationRequest:contentProto.dataMessage]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loki: Handle friend request acceptance if needed
|
||||
[LKFriendRequestProtocol handleFriendRequestAcceptanceIfNeeded:envelope in:transaction];
|
||||
|
||||
// Loki: Handle device linking message if needed
|
||||
if (contentProto.lokiDeviceLinkMessage != nil) {
|
||||
[LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope using:transaction];
|
||||
[LKMultiDeviceProtocol handleDeviceLinkMessageIfNeeded:contentProto wrappedIn:envelope transaction:transaction];
|
||||
} else if (contentProto.syncMessage) {
|
||||
[self throws_handleIncomingEnvelope:envelope
|
||||
withSyncMessage:contentProto.syncMessage
|
||||
|
@ -542,13 +564,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
|
||||
BOOL usesSharedSenderKeys = [LKClosedGroupsProtocol handleSharedSenderKeysUpdateIfNeeded:dataMessage transaction:transaction];
|
||||
|
||||
if (dataMessage.group) {
|
||||
TSGroupThread *_Nullable groupThread =
|
||||
[TSGroupThread threadWithGroupId:dataMessage.group.id transaction:transaction];
|
||||
|
||||
if (groupThread) {
|
||||
// Loki: Ignore closed group message if needed
|
||||
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupMessage:dataMessage inThread:groupThread wrappedIn:envelope using:transaction]) { return; }
|
||||
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupMessage:dataMessage inThread:groupThread wrappedIn:envelope]) { return; }
|
||||
|
||||
if (dataMessage.group.type != SSKProtoGroupContextTypeUpdate) {
|
||||
if (![groupThread isCurrentUserInGroupWithTransaction:transaction]) {
|
||||
|
@ -557,9 +581,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// Unknown group.
|
||||
// Unknown group
|
||||
if (dataMessage.group.type == SSKProtoGroupContextTypeUpdate) {
|
||||
// Accept group updates for unknown groups.
|
||||
// Accept group updates for unknown groups
|
||||
} else if (dataMessage.group.type == SSKProtoGroupContextTypeDeliver) {
|
||||
[self sendGroupInfoRequest:dataMessage.group.id envelope:envelope transaction:transaction];
|
||||
return;
|
||||
|
@ -577,7 +601,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
} else if ((dataMessage.flags & SSKProtoDataMessageFlagsProfileKeyUpdate) != 0) {
|
||||
[self handleProfileKeyMessageWithEnvelope:envelope dataMessage:dataMessage];
|
||||
} else if ([LKMultiDeviceProtocol isUnlinkDeviceMessage:dataMessage]) {
|
||||
[LKMultiDeviceProtocol handleUnlinkDeviceMessage:dataMessage wrappedIn:envelope using:transaction];
|
||||
[LKMultiDeviceProtocol handleUnlinkDeviceMessage:dataMessage wrappedIn:envelope transaction:transaction];
|
||||
} else if (dataMessage.attachments.count > 0) {
|
||||
[self handleReceivedMediaWithEnvelope:envelope
|
||||
dataMessage:dataMessage
|
||||
|
@ -846,6 +870,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction];
|
||||
|
||||
if (!thread) {
|
||||
OWSFailDebug(@"Ignoring media message for unknown group.");
|
||||
return;
|
||||
|
@ -895,7 +920,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Loki: Take into account multi device when checking sync message validity
|
||||
if (![LKSyncMessagesProtocol isValidSyncMessage:envelope in:transaction]) {
|
||||
if (![LKSyncMessagesProtocol isValidSyncMessage:envelope transaction:transaction]) {
|
||||
OWSProdErrorWEnvelope([OWSAnalyticsEvents messageManagerErrorSyncMessageFromUnknownSource], envelope);
|
||||
return;
|
||||
}
|
||||
|
@ -911,7 +936,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Loki: Update profile if needed (i.e. if the sync message came from the master device)
|
||||
[LKSyncMessagesProtocol updateProfileFromSyncMessageIfNeeded:dataMessage wrappedIn:envelope using:transaction];
|
||||
[LKSyncMessagesProtocol updateProfileFromSyncMessageIfNeeded:dataMessage wrappedIn:envelope transaction:transaction];
|
||||
|
||||
NSString *destination = syncMessage.sent.destination;
|
||||
if (dataMessage && destination.length > 0 && dataMessage.hasProfileKey) {
|
||||
|
@ -950,10 +975,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
} else {
|
||||
if (transcript.isGroupUpdate) {
|
||||
// Loki: Handle closed group updated sync message
|
||||
[LKSyncMessagesProtocol handleClosedGroupUpdatedSyncMessageIfNeeded:transcript using:transaction];
|
||||
[LKSyncMessagesProtocol handleClosedGroupUpdateSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction];
|
||||
} else if (transcript.isGroupQuit) {
|
||||
// Loki: Handle closed group quit sync message
|
||||
[LKSyncMessagesProtocol handleClosedGroupQuitSyncMessageIfNeeded:transcript using:transaction];
|
||||
[LKSyncMessagesProtocol handleClosedGroupQuitSyncMessageIfNeeded:transcript wrappedIn:envelope transaction:transaction];
|
||||
} else {
|
||||
[OWSRecordTranscriptJob
|
||||
processIncomingSentMessageTranscript:transcript
|
||||
|
@ -1003,13 +1028,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
[self.identityManager throws_processIncomingSyncMessage:syncMessage.verified transaction:transaction];
|
||||
} else if (syncMessage.contacts != nil) {
|
||||
// Loki: Handle contact sync message
|
||||
[LKSyncMessagesProtocol handleContactSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
|
||||
[LKSyncMessagesProtocol handleContactSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
|
||||
} else if (syncMessage.groups != nil) {
|
||||
// Loki: Handle closed groups sync message
|
||||
[LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
|
||||
[LKSyncMessagesProtocol handleClosedGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
|
||||
} else if (syncMessage.openGroups != nil) {
|
||||
// Loki: Handle open groups sync message
|
||||
[LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope using:transaction];
|
||||
[LKSyncMessagesProtocol handleOpenGroupSyncMessageIfNeeded:syncMessage wrappedIn:envelope transaction:transaction];
|
||||
} else {
|
||||
OWSLogWarn(@"Ignoring unsupported sync message.");
|
||||
}
|
||||
|
@ -1033,8 +1058,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Loki: Handle session reset
|
||||
NSString *sender = envelope.source;
|
||||
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:sender transaction:transaction];
|
||||
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:envelope.source transaction:transaction];
|
||||
[LKSessionManagementProtocol handleEndSessionMessageReceivedInThread:thread using:transaction];
|
||||
}
|
||||
|
||||
|
@ -1057,7 +1081,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
TSThread *_Nullable thread = [self threadForEnvelope:envelope dataMessage:dataMessage transaction:transaction];
|
||||
if (!thread) {
|
||||
OWSFailDebug(@"ignoring expiring messages update for unknown group.");
|
||||
OWSFailDebug(@"Ignoring expiring messages update for unknown group.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1107,6 +1131,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSFailDebug(@"Received a profile key message without a profile key from: %@.", envelopeAddress(envelope));
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *profileKey = dataMessage.profileKey;
|
||||
if (profileKey.length != kAES256_KeyByteLength) {
|
||||
OWSFailDebug(@"received profile key of unexpected length: %lu, from: %@",
|
||||
|
@ -1263,7 +1288,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if ([ECKeyPair isValidHexEncodedPublicKeyWithCandidate:envelope.source] && dataMessage.publicChatInfo == nil) { // Handled in LokiPublicChatPoller for open group messages
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
|
||||
[[LKMultiDeviceProtocol updateDeviceLinksIfNeededForHexEncodedPublicKey:envelope.source in:transaction].ensureOn(queue, ^() {
|
||||
[[LKMultiDeviceProtocol updateDeviceLinksIfNeededForPublicKey:envelope.source transaction:transaction].ensureOn(queue, ^() {
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
}).catchOn(queue, ^(NSError *error) {
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
|
@ -1277,16 +1302,15 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSMutableSet *removedMemberIds = [NSMutableSet new];
|
||||
for (NSString *recipientId in newMemberIds) {
|
||||
if (![ECKeyPair isValidHexEncodedPublicKeyWithCandidate:recipientId]) {
|
||||
OWSLogVerbose(
|
||||
@"incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]);
|
||||
OWSFailDebug(@"incoming group update has invalid group member");
|
||||
OWSLogVerbose(@"Incoming group update has invalid group member: %@", [self descriptionForEnvelope:envelope]);
|
||||
OWSFailDebug(@"Incoming group update has invalid group member");
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
NSString *senderMasterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSString *userMasterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userHexEncodedPublicKey in:transaction] ?: userHexEncodedPublicKey);
|
||||
NSString *senderMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
NSString *userPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSString *userMasterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:userPublicKey in:transaction] ?: userPublicKey);
|
||||
|
||||
// Group messages create the group if it doesn't already exist.
|
||||
//
|
||||
|
@ -1306,18 +1330,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
// Loki: Handle profile key update if needed
|
||||
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:senderMasterHexEncodedPublicKey using:dataMessage];
|
||||
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:senderMasterPublicKey using:dataMessage];
|
||||
|
||||
// Loki: Handle display name update if needed
|
||||
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:senderMasterHexEncodedPublicKey using:dataMessage transaction:transaction];
|
||||
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:senderMasterPublicKey using:dataMessage transaction:transaction];
|
||||
|
||||
switch (dataMessage.group.type) {
|
||||
case SSKProtoGroupContextTypeUpdate: {
|
||||
// Loki: Ignore updates from non-admins
|
||||
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope using:transaction]) {
|
||||
if ([LKClosedGroupsProtocol shouldIgnoreClosedGroupUpdateMessage:dataMessage inThread:oldGroupThread wrappedIn:envelope]) {
|
||||
return nil;
|
||||
}
|
||||
// Ensures that the thread exists but doesn't update it.
|
||||
// Ensures that the thread exists but don't update it.
|
||||
TSGroupThread *newGroupThread =
|
||||
[TSGroupThread getOrCreateThreadWithGroupId:groupId groupType:oldGroupThread.groupModel.groupType transaction:transaction];
|
||||
|
||||
|
@ -1333,10 +1357,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[newGroupThread setGroupModel:newGroupModel withTransaction:transaction];
|
||||
|
||||
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterHexEncodedPublicKey];
|
||||
BOOL wasCurrentUserRemovedFromGroup = [removedMemberIds containsObject:userMasterPublicKey];
|
||||
if (!wasCurrentUserRemovedFromGroup) {
|
||||
// Loki: Try to establish sessions with all members when a group is created or updated
|
||||
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects inThread:newGroupThread using:transaction];
|
||||
if (!newGroupThread.usesSharedSenderKeys) {
|
||||
// Loki: Try to establish sessions with all members involved when a group is created or updated
|
||||
[LKClosedGroupsProtocol establishSessionsIfNeededWithClosedGroupMembers:newMemberIds.allObjects inThread:newGroupThread transaction:transaction];
|
||||
}
|
||||
}
|
||||
|
||||
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
|
||||
|
@ -1365,12 +1391,12 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
return nil;
|
||||
}
|
||||
newMemberIds = [NSMutableSet setWithArray:oldGroupThread.groupModel.groupMemberIds];
|
||||
[newMemberIds removeObject:senderMasterHexEncodedPublicKey];
|
||||
[newMemberIds removeObject:senderMasterPublicKey];
|
||||
oldGroupThread.groupModel.groupMemberIds = [newMemberIds.allObjects mutableCopy];
|
||||
[oldGroupThread saveWithTransaction:transaction];
|
||||
|
||||
NSString *nameString =
|
||||
[self.contactsManager displayNameForPhoneIdentifier:senderMasterHexEncodedPublicKey transaction:transaction];
|
||||
[self.contactsManager displayNameForPhoneIdentifier:senderMasterPublicKey transaction:transaction];
|
||||
NSString *updateGroupInfo =
|
||||
[NSString stringWithFormat:NSLocalizedString(@"GROUP_MEMBER_LEFT", @""), nameString];
|
||||
// MJK TODO - should be safe to remove senderTimestamp
|
||||
|
@ -1382,7 +1408,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// If we were the one that quit then we need to leave the group (only relevant for slave
|
||||
// devices in a multi device context)
|
||||
// TODO: This needs more documentation
|
||||
if (![newMemberIds containsObject:userMasterHexEncodedPublicKey]) {
|
||||
if (![newMemberIds containsObject:userMasterPublicKey]) {
|
||||
[oldGroupThread leaveGroupWithTransaction:transaction];
|
||||
}
|
||||
|
||||
|
@ -1390,13 +1416,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
case SSKProtoGroupContextTypeDeliver: {
|
||||
if (!oldGroupThread) {
|
||||
OWSFailDebug(@"ignoring deliver group message from unknown group.");
|
||||
OWSFailDebug(@"Ignoring deliver group message from unknown group.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
|
||||
thread:oldGroupThread
|
||||
createdByRemoteRecipientId:senderMasterHexEncodedPublicKey
|
||||
createdByRemoteRecipientId:senderMasterPublicKey
|
||||
createdInExistingGroup:NO
|
||||
transaction:transaction];
|
||||
|
||||
|
@ -1414,7 +1440,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSLogError(@"linkPreviewError: %@", linkPreviewError);
|
||||
}
|
||||
|
||||
OWSLogDebug(@"incoming message from: %@ for group: %@ with timestamp: %lu",
|
||||
OWSLogDebug(@"Incoming message from: %@ for group: %@ with timestamp: %lu",
|
||||
envelopeAddress(envelope),
|
||||
groupId,
|
||||
(unsigned long)timestamp);
|
||||
|
@ -1423,7 +1449,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
TSIncomingMessage *incomingMessage =
|
||||
[[TSIncomingMessage alloc] initIncomingMessageWithTimestamp:timestamp
|
||||
inThread:oldGroupThread
|
||||
authorId:senderMasterHexEncodedPublicKey
|
||||
authorId:senderMasterPublicKey
|
||||
sourceDeviceId:envelope.sourceDevice
|
||||
messageBody:body
|
||||
attachmentIds:@[]
|
||||
|
@ -1449,13 +1475,13 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
|
||||
OWSLogWarn(@"Ignoring empty incoming message from: %@ for group: %@ with timestamp: %lu.",
|
||||
senderMasterHexEncodedPublicKey,
|
||||
senderMasterPublicKey,
|
||||
groupId,
|
||||
(unsigned long)timestamp);
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Loki: Cache the user hex encoded public key (for mentions)
|
||||
// Loki: Cache the user public key (for mentions)
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[LKMentionsManager populateUserPublicKeyCacheIfNeededFor:oldGroupThread.uniqueId in:transaction];
|
||||
|
@ -1486,16 +1512,16 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
// Loki: A message from a slave device should appear as if it came from the master device; the underlying
|
||||
// friend request logic, however, should still be specific to the slave device.
|
||||
|
||||
NSString *hexEncodedPublicKey = envelope.source;
|
||||
NSString *masterHexEncodedPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:hexEncodedPublicKey transaction:transaction];
|
||||
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterHexEncodedPublicKey transaction:transaction];
|
||||
NSString *publicKey = envelope.source;
|
||||
NSString *masterPublicKey = ([LKDatabaseUtilities getMasterHexEncodedPublicKeyFor:envelope.source in:transaction] ?: envelope.source);
|
||||
TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:publicKey transaction:transaction];
|
||||
TSContactThread *masterThread = [TSContactThread getOrCreateThreadWithContactId:masterPublicKey transaction:transaction];
|
||||
|
||||
OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp);
|
||||
OWSLogDebug(@"Incoming message from: %@ with timestamp: %lu.", publicKey, (unsigned long)timestamp);
|
||||
|
||||
[[OWSDisappearingMessagesJob sharedJob] becomeConsistentWithDisappearingDuration:dataMessage.expireTimer
|
||||
thread:masterThread
|
||||
createdByRemoteRecipientId:hexEncodedPublicKey
|
||||
createdByRemoteRecipientId:publicKey
|
||||
createdInExistingGroup:NO
|
||||
transaction:transaction];
|
||||
|
||||
|
@ -1528,7 +1554,10 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
serverTimestamp:serverTimestamp
|
||||
wasReceivedByUD:wasReceivedByUD];
|
||||
|
||||
// Loki: Handle display name update if needed
|
||||
[LKSessionMetaProtocol updateDisplayNameIfNeededForPublicKey:incomingMessage.authorId using:dataMessage transaction:transaction];
|
||||
|
||||
// Loki: Handle profile key update if needed
|
||||
[LKSessionMetaProtocol updateProfileKeyIfNeededForPublicKey:thread.contactIdentifier using:dataMessage];
|
||||
|
||||
NSArray<TSAttachmentPointer *> *attachmentPointers =
|
||||
|
@ -1544,33 +1573,22 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
if (body.length == 0 && attachmentPointers.count < 1 && !contact) {
|
||||
if (envelope.type == SSKProtoEnvelopeTypeFriendRequest) {
|
||||
// Loki: This is needed for compatibility with refactored desktop clients
|
||||
[LKSessionManagementProtocol sendSessionEstablishedMessageToPublicKey:hexEncodedPublicKey in:transaction];
|
||||
[LKSessionManagementProtocol sendSessionEstablishedMessageToPublicKey:publicKey transaction:transaction];
|
||||
} else {
|
||||
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.", hexEncodedPublicKey, (unsigned long)timestamp);
|
||||
OWSLogWarn(@"Ignoring empty incoming message from: %@ with timestamp: %lu.", publicKey, (unsigned long)timestamp);
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Loki: This is needed for compatibility with refactored desktop clients
|
||||
LKFriendRequestStatus friendRequestStatus = [self.primaryStorage getFriendRequestStatusForContact:hexEncodedPublicKey transaction:transaction];
|
||||
LKFriendRequestStatus friendRequestStatus = [self.primaryStorage getFriendRequestStatusForContact:publicKey transaction:transaction];
|
||||
if (friendRequestStatus == LKFriendRequestStatusNone || friendRequestStatus == LKFriendRequestStatusRequestExpired) {
|
||||
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusRequestReceived forContact:hexEncodedPublicKey transaction:transaction];
|
||||
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusRequestReceived forContact:publicKey transaction:transaction];
|
||||
} else if (friendRequestStatus == LKFriendRequestStatusRequestSent) {
|
||||
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusFriends forContact:hexEncodedPublicKey transaction:transaction];
|
||||
[LKFriendRequestProtocol sendFriendRequestAcceptedMessageToPublicKey:hexEncodedPublicKey using:transaction];
|
||||
[LKSyncMessagesProtocol syncContactWithPublicKey:masterHexEncodedPublicKey in:transaction];
|
||||
[self.primaryStorage setFriendRequestStatus:LKFriendRequestStatusFriends forContact:publicKey transaction:transaction];
|
||||
[LKFriendRequestProtocol sendFriendRequestAcceptedMessageToPublicKey:publicKey using:transaction];
|
||||
[LKSyncMessagesProtocol syncContactWithPublicKey:masterPublicKey];
|
||||
}
|
||||
|
||||
// Loki: If we received a message from a contact in the last 2 minutes that wasn't P2P, then we need to ping them.
|
||||
// We assume this occurred because they don't have our P2P details.
|
||||
/*
|
||||
if (!envelope.isPtpMessage && hexEncodedPublicKey != nil) {
|
||||
uint64_t timestamp = envelope.timestamp;
|
||||
uint64_t now = NSDate.ows_millisecondTimeStamp;
|
||||
uint64_t ageInSeconds = (now - timestamp) / 1000;
|
||||
if (ageInSeconds <= 120) { [LKP2PAPI pingContact:hexEncodedPublicKey]; }
|
||||
}
|
||||
*/
|
||||
|
||||
[self finalizeIncomingMessage:incomingMessage
|
||||
thread:thread
|
||||
|
|
|
@ -125,6 +125,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
|
|||
- (OWSMessageDecryptJob *_Nullable)nextJob
|
||||
{
|
||||
__block OWSMessageDecryptJob *_Nullable job = nil;
|
||||
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||
YapDatabaseViewTransaction *viewTransaction = [transaction ext:OWSMessageDecryptJobFinderExtensionName];
|
||||
OWSAssertDebug(viewTransaction != nil);
|
||||
|
@ -406,7 +407,7 @@ NSString *const OWSMessageDecryptJobFinderExtensionGroup = @"OWSMessageProcessin
|
|||
}
|
||||
|
||||
// We persist the decrypted envelope data in the same transaction within which
|
||||
// it was decrypted to prevent data loss. If the new job isn't persisted,
|
||||
// it was decrypted to prevent data loss. If the new job isn't persisted,
|
||||
// the session state side effects of its decryption are also rolled back.
|
||||
//
|
||||
// NOTE: We use envelopeData from the decrypt result, not job.envelopeData,
|
||||
|
|
|
@ -612,7 +612,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
|
||||
if ([LKMultiDeviceProtocol isMultiDeviceRequiredForMessage:message]) { // Avoid the write transaction if possible
|
||||
[self.primaryStorage.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend in:transaction];
|
||||
[LKMultiDeviceProtocol sendMessageToDestinationAndLinkedDevices:messageSend transaction:transaction];
|
||||
}];
|
||||
} else {
|
||||
[self sendMessage:messageSend];
|
||||
|
@ -944,7 +944,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) {
|
||||
OWSProdError([OWSAnalyticsEvents messageSendErrorFailedDueToPrekeyUpdateFailures]);
|
||||
|
||||
// Retry pr ekey update every time user tries to send a message while the app
|
||||
// Retry pre key update every time user tries to send a message while the app
|
||||
// is disabled due to pre key update failures.
|
||||
//
|
||||
// Only try to update the signed pre key; updating it is sufficient to
|
||||
|
@ -1142,7 +1142,6 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
}
|
||||
} error:nil];
|
||||
// Convenience
|
||||
void (^onP2PSuccess)() = ^() { message.isP2P = YES; };
|
||||
void (^handleError)(NSError *error) = ^(NSError *error) {
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
if (!message.skipSave) {
|
||||
|
@ -1157,7 +1156,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
failedMessageSend(error);
|
||||
};
|
||||
// Send the message
|
||||
[[LKAPI sendSignalMessage:signalMessage onP2PSuccess:onP2PSuccess]
|
||||
[[LKSnodeAPI sendSignalMessage:signalMessage]
|
||||
.thenOn(OWSDispatch.sendingQueue, ^(id result) {
|
||||
NSSet<AnyPromise *> *promises = (NSSet<AnyPromise *> *)result;
|
||||
__block BOOL isSuccess = NO;
|
||||
|
@ -1291,7 +1290,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
switch (statusCode) {
|
||||
case 0: { // Loki
|
||||
NSError *error;
|
||||
if ([responseError isKindOfClass:LokiAPIError.class] || [responseError isKindOfClass:LokiDotNetAPIError.class]
|
||||
if ([responseError isKindOfClass:LKSnodeAPIError.class] || [responseError isKindOfClass:LKDotNetAPIError.class]
|
||||
|| [responseError isKindOfClass:DiffieHellmanError.class]) {
|
||||
error = responseError;
|
||||
} else {
|
||||
|
@ -1672,10 +1671,10 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
TSOutgoingMessage *message = messageSend.message;
|
||||
|
||||
ECKeyPair *identityKeyPair = self.identityManager.identityKeyPair;
|
||||
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientId:recipientId privateKey:identityKeyPair.privateKey];
|
||||
FallBackSessionCipher *cipher = [[FallBackSessionCipher alloc] initWithRecipientPublicKey:recipientId privateKey:identityKeyPair.privateKey];
|
||||
|
||||
// This will return nil if encryption failed
|
||||
NSData *_Nullable serializedMessage = [cipher encryptWithMessage:[plainText paddedMessageBody]];
|
||||
NSData *_Nullable serializedMessage = [cipher encrypt:[plainText paddedMessageBody]];
|
||||
if (!serializedMessage) {
|
||||
OWSFailDebug(@"Failed to encrypt friend request for: %@.", recipientId);
|
||||
return nil;
|
||||
|
@ -1752,13 +1751,47 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException";
|
|||
OWSRaiseException(@"SecretSessionCipherFailure", @"Can't create secret session cipher.");
|
||||
}
|
||||
|
||||
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
|
||||
deviceId:@(OWSDevicePrimaryDeviceId).intValue
|
||||
paddedPlaintext:[plainText paddedMessageBody]
|
||||
senderCertificate:messageSend.senderCertificate
|
||||
protocolContext:transaction
|
||||
useFallbackSessionCipher:isFriendRequestMessage || isSessionRequestMessage || isDeviceLinkMessage
|
||||
error:&error];
|
||||
if ([messageSend.thread isKindOfClass:TSGroupThread.class] && ((TSGroupThread *)messageSend.thread).usesSharedSenderKeys) {
|
||||
NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:((TSGroupThread *)messageSend.thread).groupModel.groupId];
|
||||
NSString *senderPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey;
|
||||
NSArray *ciphertextAndKeyIndex = [LKClosedGroupsProtocol encryptPlaintext:plainText.paddedMessageBody forGroupWithPublicKey:groupPublicKey senderPublicKey:senderPublicKey];
|
||||
if (ciphertextAndKeyIndex.count != 2) {
|
||||
OWSFailDebug(@"Couldn't encrypt closed group message.");
|
||||
return nil;
|
||||
}
|
||||
NSData *ivAndCiphertext = ciphertextAndKeyIndex[0];
|
||||
NSNumber *keyIndex = ciphertextAndKeyIndex[1];
|
||||
SSKProtoClosedGroupCiphertextBuilder *builder = [SSKProtoClosedGroupCiphertext builderWithCiphertext:ivAndCiphertext senderPublicKey:senderPublicKey keyIndex:keyIndex.unsignedIntValue];
|
||||
SSKProtoClosedGroupCiphertext *closedGroupCiphertext = [builder buildAndReturnError:&error];
|
||||
if (closedGroupCiphertext == nil) {
|
||||
OWSFailDebug(@"Couldn't build closed group message due to error: %@.", error);
|
||||
return nil;
|
||||
}
|
||||
ECKeyPair *keyPair = [LKStorage getKeyPairForClosedGroupWithPublicKey:groupPublicKey];
|
||||
if (keyPair == nil) {
|
||||
OWSFailDebug(@"Missing key pair for closed group with public key: %@.", groupPublicKey);
|
||||
return nil;
|
||||
}
|
||||
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
|
||||
deviceId:@(OWSDevicePrimaryDeviceId).intValue
|
||||
paddedPlaintext:nil
|
||||
closedGroupCiphertext:closedGroupCiphertext
|
||||
senderCertificate:messageSend.senderCertificate
|
||||
keyPair:keyPair
|
||||
protocolContext:transaction
|
||||
useFallbackSessionCipher:NO
|
||||
error:&error];
|
||||
} else {
|
||||
serializedMessage = [secretCipher throwswrapped_encryptMessageWithRecipientId:recipientID
|
||||
deviceId:@(OWSDevicePrimaryDeviceId).intValue
|
||||
paddedPlaintext:plainText.paddedMessageBody
|
||||
closedGroupCiphertext:nil
|
||||
senderCertificate:messageSend.senderCertificate
|
||||
keyPair:nil
|
||||
protocolContext:transaction
|
||||
useFallbackSessionCipher:isFriendRequestMessage || isSessionRequestMessage || isDeviceLinkMessage
|
||||
error:&error];
|
||||
}
|
||||
|
||||
SCKRaiseIfExceptionWrapperError(error);
|
||||
if (!serializedMessage || error) {
|
||||
|
|
|
@ -174,7 +174,7 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
|
||||
TSThread *thread = [TSContactThread getOrCreateThreadWithContactId:recipientId];
|
||||
|
||||
if (![LKSessionMetaProtocol shouldSendReceiptForThread:thread]) {
|
||||
if (![LKSessionMetaProtocol shouldSendReceiptInThread:thread]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -286,7 +286,7 @@ NSString *const OWSReadReceiptManagerAreReadReceiptsEnabled = @"areReadReceiptsE
|
|||
self.toLinkedDevicesReadReceiptMap[threadUniqueId] = newReadReceipt;
|
||||
}
|
||||
|
||||
if (![LKSessionMetaProtocol shouldSendReceiptForThread:message.thread]) {
|
||||
if (![LKSessionMetaProtocol shouldSendReceiptInThread:message.thread]) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -152,9 +152,8 @@ const int32_t kGroupIdLength = 16;
|
|||
updatedGroupInfoString = [updatedGroupInfoString
|
||||
stringByAppendingString:NSLocalizedString(@"YOU_WERE_REMOVED", @"")];
|
||||
} else {
|
||||
NSArray *removedMemberNames = [[newModel.removedMembers allObjects] map:^NSString*(NSString* item) {
|
||||
// TODO: Shouldn't this use DisplayNameUtilities?
|
||||
return [contactsManager displayNameForPhoneIdentifier:item];
|
||||
NSArray *removedMemberNames = [newModel.removedMembers.allObjects map:^NSString*(NSString* publicKey) {
|
||||
return [LKUserDisplayNameUtilities getPrivateChatDisplayNameFor:publicKey];
|
||||
}];
|
||||
if ([removedMemberNames count] > 1) {
|
||||
updatedGroupInfoString = [updatedGroupInfoString
|
||||
|
|
|
@ -23,6 +23,7 @@ public enum SSKProtoError: Error {
|
|||
case prekeyBundle = 3
|
||||
case receipt = 5
|
||||
case unidentifiedSender = 6
|
||||
case closedGroupCiphertext = 7
|
||||
case friendRequest = 101
|
||||
}
|
||||
|
||||
|
@ -34,6 +35,7 @@ public enum SSKProtoError: Error {
|
|||
case .prekeyBundle: return .prekeyBundle
|
||||
case .receipt: return .receipt
|
||||
case .unidentifiedSender: return .unidentifiedSender
|
||||
case .closedGroupCiphertext: return .closedGroupCiphertext
|
||||
case .friendRequest: return .friendRequest
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +48,7 @@ public enum SSKProtoError: Error {
|
|||
case .prekeyBundle: return .prekeyBundle
|
||||
case .receipt: return .receipt
|
||||
case .unidentifiedSender: return .unidentifiedSender
|
||||
case .closedGroupCiphertext: return .closedGroupCiphertext
|
||||
case .friendRequest: return .friendRequest
|
||||
}
|
||||
}
|
||||
|
@ -1707,6 +1710,133 @@ extension SSKProtoCallMessage.SSKProtoCallMessageBuilder {
|
|||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoClosedGroupCiphertext
|
||||
|
||||
@objc public class SSKProtoClosedGroupCiphertext: NSObject {
|
||||
|
||||
// MARK: - SSKProtoClosedGroupCiphertextBuilder
|
||||
|
||||
@objc public class func builder(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) -> SSKProtoClosedGroupCiphertextBuilder {
|
||||
return SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc public func asBuilder() -> SSKProtoClosedGroupCiphertextBuilder {
|
||||
let builder = SSKProtoClosedGroupCiphertextBuilder(ciphertext: ciphertext, senderPublicKey: senderPublicKey, keyIndex: keyIndex)
|
||||
return builder
|
||||
}
|
||||
|
||||
@objc public class SSKProtoClosedGroupCiphertextBuilder: NSObject {
|
||||
|
||||
private var proto = SignalServiceProtos_ClosedGroupCiphertext()
|
||||
|
||||
@objc fileprivate override init() {}
|
||||
|
||||
@objc fileprivate init(ciphertext: Data, senderPublicKey: String, keyIndex: UInt32) {
|
||||
super.init()
|
||||
|
||||
setCiphertext(ciphertext)
|
||||
setSenderPublicKey(senderPublicKey)
|
||||
setKeyIndex(keyIndex)
|
||||
}
|
||||
|
||||
@objc public func setCiphertext(_ valueParam: Data) {
|
||||
proto.ciphertext = valueParam
|
||||
}
|
||||
|
||||
@objc public func setSenderPublicKey(_ valueParam: String) {
|
||||
proto.senderPublicKey = valueParam
|
||||
}
|
||||
|
||||
@objc public func setKeyIndex(_ valueParam: UInt32) {
|
||||
proto.keyIndex = valueParam
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoClosedGroupCiphertext {
|
||||
return try SSKProtoClosedGroupCiphertext.parseProto(proto)
|
||||
}
|
||||
|
||||
@objc public func buildSerializedData() throws -> Data {
|
||||
return try SSKProtoClosedGroupCiphertext.parseProto(proto).serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let proto: SignalServiceProtos_ClosedGroupCiphertext
|
||||
|
||||
@objc public let ciphertext: Data
|
||||
|
||||
@objc public let senderPublicKey: String
|
||||
|
||||
@objc public let keyIndex: UInt32
|
||||
|
||||
private init(proto: SignalServiceProtos_ClosedGroupCiphertext,
|
||||
ciphertext: Data,
|
||||
senderPublicKey: String,
|
||||
keyIndex: UInt32) {
|
||||
self.proto = proto
|
||||
self.ciphertext = ciphertext
|
||||
self.senderPublicKey = senderPublicKey
|
||||
self.keyIndex = keyIndex
|
||||
}
|
||||
|
||||
@objc
|
||||
public func serializedData() throws -> Data {
|
||||
return try self.proto.serializedData()
|
||||
}
|
||||
|
||||
@objc public class func parseData(_ serializedData: Data) throws -> SSKProtoClosedGroupCiphertext {
|
||||
let proto = try SignalServiceProtos_ClosedGroupCiphertext(serializedData: serializedData)
|
||||
return try parseProto(proto)
|
||||
}
|
||||
|
||||
fileprivate class func parseProto(_ proto: SignalServiceProtos_ClosedGroupCiphertext) throws -> SSKProtoClosedGroupCiphertext {
|
||||
guard proto.hasCiphertext else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: ciphertext")
|
||||
}
|
||||
let ciphertext = proto.ciphertext
|
||||
|
||||
guard proto.hasSenderPublicKey else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: senderPublicKey")
|
||||
}
|
||||
let senderPublicKey = proto.senderPublicKey
|
||||
|
||||
guard proto.hasKeyIndex else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: keyIndex")
|
||||
}
|
||||
let keyIndex = proto.keyIndex
|
||||
|
||||
// MARK: - Begin Validation Logic for SSKProtoClosedGroupCiphertext -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoClosedGroupCiphertext -
|
||||
|
||||
let result = SSKProtoClosedGroupCiphertext(proto: proto,
|
||||
ciphertext: ciphertext,
|
||||
senderPublicKey: senderPublicKey,
|
||||
keyIndex: keyIndex)
|
||||
return result
|
||||
}
|
||||
|
||||
@objc public override var debugDescription: String {
|
||||
return "\(proto)"
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
extension SSKProtoClosedGroupCiphertext {
|
||||
@objc public func serializedDataIgnoringErrors() -> Data? {
|
||||
return try! self.serializedData()
|
||||
}
|
||||
}
|
||||
|
||||
extension SSKProtoClosedGroupCiphertext.SSKProtoClosedGroupCiphertextBuilder {
|
||||
@objc public func buildIgnoringErrors() -> SSKProtoClosedGroupCiphertext? {
|
||||
return try! self.build()
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// MARK: - SSKProtoDataMessageQuoteQuotedAttachment
|
||||
|
||||
@objc public class SSKProtoDataMessageQuoteQuotedAttachment: NSObject {
|
||||
|
@ -3288,16 +3418,42 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
|
||||
@objc public class SSKProtoDataMessageClosedGroupUpdate: NSObject {
|
||||
|
||||
// MARK: - SSKProtoDataMessageClosedGroupUpdateType
|
||||
|
||||
@objc public enum SSKProtoDataMessageClosedGroupUpdateType: Int32 {
|
||||
case new = 0
|
||||
}
|
||||
|
||||
private class func SSKProtoDataMessageClosedGroupUpdateTypeWrap(_ value: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum) -> SSKProtoDataMessageClosedGroupUpdateType {
|
||||
switch value {
|
||||
case .new: return .new
|
||||
}
|
||||
}
|
||||
|
||||
private class func SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(_ value: SSKProtoDataMessageClosedGroupUpdateType) -> SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum {
|
||||
switch value {
|
||||
case .new: return .new
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SSKProtoDataMessageClosedGroupUpdateBuilder
|
||||
|
||||
@objc public class func builder(name: String, groupID: String, sharedSecret: String, senderKey: String) -> SSKProtoDataMessageClosedGroupUpdateBuilder {
|
||||
return SSKProtoDataMessageClosedGroupUpdateBuilder(name: name, groupID: groupID, sharedSecret: sharedSecret, senderKey: senderKey)
|
||||
@objc public class func builder(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) -> SSKProtoDataMessageClosedGroupUpdateBuilder {
|
||||
return SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type)
|
||||
}
|
||||
|
||||
// asBuilder() constructs a builder that reflects the proto's contents.
|
||||
@objc public func asBuilder() -> SSKProtoDataMessageClosedGroupUpdateBuilder {
|
||||
let builder = SSKProtoDataMessageClosedGroupUpdateBuilder(name: name, groupID: groupID, sharedSecret: sharedSecret, senderKey: senderKey)
|
||||
let builder = SSKProtoDataMessageClosedGroupUpdateBuilder(groupPublicKey: groupPublicKey, type: type)
|
||||
if let _value = name {
|
||||
builder.setName(_value)
|
||||
}
|
||||
if let _value = groupPrivateKey {
|
||||
builder.setGroupPrivateKey(_value)
|
||||
}
|
||||
builder.setChainKeys(chainKeys)
|
||||
builder.setMembers(members)
|
||||
builder.setAdmins(admins)
|
||||
return builder
|
||||
}
|
||||
|
||||
|
@ -3307,29 +3463,33 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
|
||||
@objc fileprivate override init() {}
|
||||
|
||||
@objc fileprivate init(name: String, groupID: String, sharedSecret: String, senderKey: String) {
|
||||
@objc fileprivate init(groupPublicKey: Data, type: SSKProtoDataMessageClosedGroupUpdateType) {
|
||||
super.init()
|
||||
|
||||
setName(name)
|
||||
setGroupID(groupID)
|
||||
setSharedSecret(sharedSecret)
|
||||
setSenderKey(senderKey)
|
||||
setGroupPublicKey(groupPublicKey)
|
||||
setType(type)
|
||||
}
|
||||
|
||||
@objc public func setName(_ valueParam: String) {
|
||||
proto.name = valueParam
|
||||
}
|
||||
|
||||
@objc public func setGroupID(_ valueParam: String) {
|
||||
proto.groupID = valueParam
|
||||
@objc public func setGroupPublicKey(_ valueParam: Data) {
|
||||
proto.groupPublicKey = valueParam
|
||||
}
|
||||
|
||||
@objc public func setSharedSecret(_ valueParam: String) {
|
||||
proto.sharedSecret = valueParam
|
||||
@objc public func setGroupPrivateKey(_ valueParam: Data) {
|
||||
proto.groupPrivateKey = valueParam
|
||||
}
|
||||
|
||||
@objc public func setSenderKey(_ valueParam: String) {
|
||||
proto.senderKey = valueParam
|
||||
@objc public func addChainKeys(_ valueParam: Data) {
|
||||
var items = proto.chainKeys
|
||||
items.append(valueParam)
|
||||
proto.chainKeys = items
|
||||
}
|
||||
|
||||
@objc public func setChainKeys(_ wrappedItems: [Data]) {
|
||||
proto.chainKeys = wrappedItems
|
||||
}
|
||||
|
||||
@objc public func addMembers(_ valueParam: String) {
|
||||
|
@ -3342,6 +3502,20 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
proto.members = wrappedItems
|
||||
}
|
||||
|
||||
@objc public func addAdmins(_ valueParam: String) {
|
||||
var items = proto.admins
|
||||
items.append(valueParam)
|
||||
proto.admins = items
|
||||
}
|
||||
|
||||
@objc public func setAdmins(_ wrappedItems: [String]) {
|
||||
proto.admins = wrappedItems
|
||||
}
|
||||
|
||||
@objc public func setType(_ valueParam: SSKProtoDataMessageClosedGroupUpdateType) {
|
||||
proto.type = SSKProtoDataMessageClosedGroupUpdateTypeUnwrap(valueParam)
|
||||
}
|
||||
|
||||
@objc public func build() throws -> SSKProtoDataMessageClosedGroupUpdate {
|
||||
return try SSKProtoDataMessageClosedGroupUpdate.parseProto(proto)
|
||||
}
|
||||
|
@ -3353,28 +3527,48 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
|
||||
fileprivate let proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate
|
||||
|
||||
@objc public let name: String
|
||||
@objc public let groupPublicKey: Data
|
||||
|
||||
@objc public let groupID: String
|
||||
@objc public let type: SSKProtoDataMessageClosedGroupUpdateType
|
||||
|
||||
@objc public let sharedSecret: String
|
||||
@objc public var name: String? {
|
||||
guard proto.hasName else {
|
||||
return nil
|
||||
}
|
||||
return proto.name
|
||||
}
|
||||
@objc public var hasName: Bool {
|
||||
return proto.hasName
|
||||
}
|
||||
|
||||
@objc public let senderKey: String
|
||||
@objc public var groupPrivateKey: Data? {
|
||||
guard proto.hasGroupPrivateKey else {
|
||||
return nil
|
||||
}
|
||||
return proto.groupPrivateKey
|
||||
}
|
||||
@objc public var hasGroupPrivateKey: Bool {
|
||||
return proto.hasGroupPrivateKey
|
||||
}
|
||||
|
||||
@objc public var chainKeys: [Data] {
|
||||
return proto.chainKeys
|
||||
}
|
||||
|
||||
@objc public var members: [String] {
|
||||
return proto.members
|
||||
}
|
||||
|
||||
@objc public var admins: [String] {
|
||||
return proto.admins
|
||||
}
|
||||
|
||||
private init(proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate,
|
||||
name: String,
|
||||
groupID: String,
|
||||
sharedSecret: String,
|
||||
senderKey: String) {
|
||||
groupPublicKey: Data,
|
||||
type: SSKProtoDataMessageClosedGroupUpdateType) {
|
||||
self.proto = proto
|
||||
self.name = name
|
||||
self.groupID = groupID
|
||||
self.sharedSecret = sharedSecret
|
||||
self.senderKey = senderKey
|
||||
self.groupPublicKey = groupPublicKey
|
||||
self.type = type
|
||||
}
|
||||
|
||||
@objc
|
||||
|
@ -3388,35 +3582,23 @@ extension SSKProtoDataMessageLokiProfile.SSKProtoDataMessageLokiProfileBuilder {
|
|||
}
|
||||
|
||||
fileprivate class func parseProto(_ proto: SignalServiceProtos_DataMessage.ClosedGroupUpdate) throws -> SSKProtoDataMessageClosedGroupUpdate {
|
||||
guard proto.hasName else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: name")
|
||||
guard proto.hasGroupPublicKey else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: groupPublicKey")
|
||||
}
|
||||
let name = proto.name
|
||||
let groupPublicKey = proto.groupPublicKey
|
||||
|
||||
guard proto.hasGroupID else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: groupID")
|
||||
guard proto.hasType else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: type")
|
||||
}
|
||||
let groupID = proto.groupID
|
||||
|
||||
guard proto.hasSharedSecret else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: sharedSecret")
|
||||
}
|
||||
let sharedSecret = proto.sharedSecret
|
||||
|
||||
guard proto.hasSenderKey else {
|
||||
throw SSKProtoError.invalidProtobuf(description: "\(logTag) missing required field: senderKey")
|
||||
}
|
||||
let senderKey = proto.senderKey
|
||||
let type = SSKProtoDataMessageClosedGroupUpdateTypeWrap(proto.type)
|
||||
|
||||
// MARK: - Begin Validation Logic for SSKProtoDataMessageClosedGroupUpdate -
|
||||
|
||||
// MARK: - End Validation Logic for SSKProtoDataMessageClosedGroupUpdate -
|
||||
|
||||
let result = SSKProtoDataMessageClosedGroupUpdate(proto: proto,
|
||||
name: name,
|
||||
groupID: groupID,
|
||||
sharedSecret: sharedSecret,
|
||||
senderKey: senderKey)
|
||||
groupPublicKey: groupPublicKey,
|
||||
type: type)
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
@ -131,7 +131,10 @@ struct SignalServiceProtos_Envelope {
|
|||
case receipt // = 5
|
||||
case unidentifiedSender // = 6
|
||||
|
||||
/// Loki: Contains prekeys and a message; uses simple encryption
|
||||
/// Loki
|
||||
case closedGroupCiphertext // = 7
|
||||
|
||||
/// Loki: Contains pre keys and a message; uses simple encryption
|
||||
case friendRequest // = 101
|
||||
|
||||
init() {
|
||||
|
@ -146,6 +149,7 @@ struct SignalServiceProtos_Envelope {
|
|||
case 3: self = .prekeyBundle
|
||||
case 5: self = .receipt
|
||||
case 6: self = .unidentifiedSender
|
||||
case 7: self = .closedGroupCiphertext
|
||||
case 101: self = .friendRequest
|
||||
default: return nil
|
||||
}
|
||||
|
@ -159,6 +163,7 @@ struct SignalServiceProtos_Envelope {
|
|||
case .prekeyBundle: return 3
|
||||
case .receipt: return 5
|
||||
case .unidentifiedSender: return 6
|
||||
case .closedGroupCiphertext: return 7
|
||||
case .friendRequest: return 101
|
||||
}
|
||||
}
|
||||
|
@ -724,6 +729,50 @@ struct SignalServiceProtos_CallMessage {
|
|||
fileprivate var _profileKey: Data? = nil
|
||||
}
|
||||
|
||||
struct SignalServiceProtos_ClosedGroupCiphertext {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// @required
|
||||
var ciphertext: Data {
|
||||
get {return _ciphertext ?? SwiftProtobuf.Internal.emptyData}
|
||||
set {_ciphertext = newValue}
|
||||
}
|
||||
/// Returns true if `ciphertext` has been explicitly set.
|
||||
var hasCiphertext: Bool {return self._ciphertext != nil}
|
||||
/// Clears the value of `ciphertext`. Subsequent reads from it will return its default value.
|
||||
mutating func clearCiphertext() {self._ciphertext = nil}
|
||||
|
||||
/// @required
|
||||
var senderPublicKey: String {
|
||||
get {return _senderPublicKey ?? String()}
|
||||
set {_senderPublicKey = newValue}
|
||||
}
|
||||
/// Returns true if `senderPublicKey` has been explicitly set.
|
||||
var hasSenderPublicKey: Bool {return self._senderPublicKey != nil}
|
||||
/// Clears the value of `senderPublicKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearSenderPublicKey() {self._senderPublicKey = nil}
|
||||
|
||||
/// @required
|
||||
var keyIndex: UInt32 {
|
||||
get {return _keyIndex ?? 0}
|
||||
set {_keyIndex = newValue}
|
||||
}
|
||||
/// Returns true if `keyIndex` has been explicitly set.
|
||||
var hasKeyIndex: Bool {return self._keyIndex != nil}
|
||||
/// Clears the value of `keyIndex`. Subsequent reads from it will return its default value.
|
||||
mutating func clearKeyIndex() {self._keyIndex = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _ciphertext: Data? = nil
|
||||
fileprivate var _senderPublicKey: String? = nil
|
||||
fileprivate var _keyIndex: UInt32? = nil
|
||||
}
|
||||
|
||||
struct SignalServiceProtos_DataMessage {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
@ -1454,7 +1503,7 @@ struct SignalServiceProtos_DataMessage {
|
|||
fileprivate var _image: SignalServiceProtos_AttachmentPointer? = nil
|
||||
}
|
||||
|
||||
/// Loki: A custom message for our profile
|
||||
/// Loki
|
||||
struct LokiProfile {
|
||||
// SwiftProtobuf.Message conformance is added in an extension below. See the
|
||||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
|
@ -1492,7 +1541,6 @@ struct SignalServiceProtos_DataMessage {
|
|||
// `Message` and `Message+*Additions` files in the SwiftProtobuf library for
|
||||
// methods supported on all messages.
|
||||
|
||||
/// @required
|
||||
var name: String {
|
||||
get {return _name ?? String()}
|
||||
set {_name = newValue}
|
||||
|
@ -1503,45 +1551,73 @@ struct SignalServiceProtos_DataMessage {
|
|||
mutating func clearName() {self._name = nil}
|
||||
|
||||
/// @required
|
||||
var groupID: String {
|
||||
get {return _groupID ?? String()}
|
||||
set {_groupID = newValue}
|
||||
var groupPublicKey: Data {
|
||||
get {return _groupPublicKey ?? SwiftProtobuf.Internal.emptyData}
|
||||
set {_groupPublicKey = newValue}
|
||||
}
|
||||
/// Returns true if `groupID` has been explicitly set.
|
||||
var hasGroupID: Bool {return self._groupID != nil}
|
||||
/// Clears the value of `groupID`. Subsequent reads from it will return its default value.
|
||||
mutating func clearGroupID() {self._groupID = nil}
|
||||
/// Returns true if `groupPublicKey` has been explicitly set.
|
||||
var hasGroupPublicKey: Bool {return self._groupPublicKey != nil}
|
||||
/// Clears the value of `groupPublicKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearGroupPublicKey() {self._groupPublicKey = nil}
|
||||
|
||||
/// @required
|
||||
var sharedSecret: String {
|
||||
get {return _sharedSecret ?? String()}
|
||||
set {_sharedSecret = newValue}
|
||||
var groupPrivateKey: Data {
|
||||
get {return _groupPrivateKey ?? SwiftProtobuf.Internal.emptyData}
|
||||
set {_groupPrivateKey = newValue}
|
||||
}
|
||||
/// Returns true if `sharedSecret` has been explicitly set.
|
||||
var hasSharedSecret: Bool {return self._sharedSecret != nil}
|
||||
/// Clears the value of `sharedSecret`. Subsequent reads from it will return its default value.
|
||||
mutating func clearSharedSecret() {self._sharedSecret = nil}
|
||||
/// Returns true if `groupPrivateKey` has been explicitly set.
|
||||
var hasGroupPrivateKey: Bool {return self._groupPrivateKey != nil}
|
||||
/// Clears the value of `groupPrivateKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearGroupPrivateKey() {self._groupPrivateKey = nil}
|
||||
|
||||
/// @required
|
||||
var senderKey: String {
|
||||
get {return _senderKey ?? String()}
|
||||
set {_senderKey = newValue}
|
||||
}
|
||||
/// Returns true if `senderKey` has been explicitly set.
|
||||
var hasSenderKey: Bool {return self._senderKey != nil}
|
||||
/// Clears the value of `senderKey`. Subsequent reads from it will return its default value.
|
||||
mutating func clearSenderKey() {self._senderKey = nil}
|
||||
var chainKeys: [Data] = []
|
||||
|
||||
var members: [String] = []
|
||||
|
||||
var admins: [String] = []
|
||||
|
||||
/// @required
|
||||
var type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum {
|
||||
get {return _type ?? .new}
|
||||
set {_type = newValue}
|
||||
}
|
||||
/// Returns true if `type` has been explicitly set.
|
||||
var hasType: Bool {return self._type != nil}
|
||||
/// Clears the value of `type`. Subsequent reads from it will return its default value.
|
||||
mutating func clearType() {self._type = nil}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
enum TypeEnum: SwiftProtobuf.Enum {
|
||||
typealias RawValue = Int
|
||||
|
||||
/// groupPublicKey, name, groupPrivateKey, chainKeys, members, admins
|
||||
case new // = 0
|
||||
|
||||
init() {
|
||||
self = .new
|
||||
}
|
||||
|
||||
init?(rawValue: Int) {
|
||||
switch rawValue {
|
||||
case 0: self = .new
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var rawValue: Int {
|
||||
switch self {
|
||||
case .new: return 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _name: String? = nil
|
||||
fileprivate var _groupID: String? = nil
|
||||
fileprivate var _sharedSecret: String? = nil
|
||||
fileprivate var _senderKey: String? = nil
|
||||
fileprivate var _groupPublicKey: Data? = nil
|
||||
fileprivate var _groupPrivateKey: Data? = nil
|
||||
fileprivate var _type: SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum? = nil
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
@ -1580,6 +1656,10 @@ extension SignalServiceProtos_DataMessage.Contact.PostalAddress.TypeEnum: CaseIt
|
|||
// Support synthesized by the compiler.
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: CaseIterable {
|
||||
// Support synthesized by the compiler.
|
||||
}
|
||||
|
||||
#endif // swift(>=4.2)
|
||||
|
||||
struct SignalServiceProtos_NullMessage {
|
||||
|
@ -2839,6 +2919,7 @@ extension SignalServiceProtos_Envelope.TypeEnum: SwiftProtobuf._ProtoNameProvidi
|
|||
3: .same(proto: "PREKEY_BUNDLE"),
|
||||
5: .same(proto: "RECEIPT"),
|
||||
6: .same(proto: "UNIDENTIFIED_SENDER"),
|
||||
7: .same(proto: "CLOSED_GROUP_CIPHERTEXT"),
|
||||
101: .same(proto: "FRIEND_REQUEST"),
|
||||
]
|
||||
}
|
||||
|
@ -3308,6 +3389,47 @@ extension SignalServiceProtos_CallMessage.Hangup: SwiftProtobuf.Message, SwiftPr
|
|||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_ClosedGroupCiphertext: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".ClosedGroupCiphertext"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "ciphertext"),
|
||||
2: .same(proto: "senderPublicKey"),
|
||||
3: .same(proto: "keyIndex"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularBytesField(value: &self._ciphertext)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self._senderPublicKey)
|
||||
case 3: try decoder.decodeSingularUInt32Field(value: &self._keyIndex)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
if let v = self._ciphertext {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._senderPublicKey {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._keyIndex {
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 3)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SignalServiceProtos_ClosedGroupCiphertext, rhs: SignalServiceProtos_ClosedGroupCiphertext) -> Bool {
|
||||
if lhs._ciphertext != rhs._ciphertext {return false}
|
||||
if lhs._senderPublicKey != rhs._senderPublicKey {return false}
|
||||
if lhs._keyIndex != rhs._keyIndex {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_DataMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".DataMessage"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
@ -3938,20 +4060,24 @@ extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Messa
|
|||
static let protoMessageName: String = SignalServiceProtos_DataMessage.protoMessageName + ".ClosedGroupUpdate"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "name"),
|
||||
2: .same(proto: "groupID"),
|
||||
3: .same(proto: "sharedSecret"),
|
||||
4: .same(proto: "senderKey"),
|
||||
2: .same(proto: "groupPublicKey"),
|
||||
3: .same(proto: "groupPrivateKey"),
|
||||
4: .same(proto: "chainKeys"),
|
||||
5: .same(proto: "members"),
|
||||
6: .same(proto: "admins"),
|
||||
7: .same(proto: "type"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
switch fieldNumber {
|
||||
case 1: try decoder.decodeSingularStringField(value: &self._name)
|
||||
case 2: try decoder.decodeSingularStringField(value: &self._groupID)
|
||||
case 3: try decoder.decodeSingularStringField(value: &self._sharedSecret)
|
||||
case 4: try decoder.decodeSingularStringField(value: &self._senderKey)
|
||||
case 2: try decoder.decodeSingularBytesField(value: &self._groupPublicKey)
|
||||
case 3: try decoder.decodeSingularBytesField(value: &self._groupPrivateKey)
|
||||
case 4: try decoder.decodeRepeatedBytesField(value: &self.chainKeys)
|
||||
case 5: try decoder.decodeRepeatedStringField(value: &self.members)
|
||||
case 6: try decoder.decodeRepeatedStringField(value: &self.admins)
|
||||
case 7: try decoder.decodeSingularEnumField(value: &self._type)
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
@ -3961,32 +4087,46 @@ extension SignalServiceProtos_DataMessage.ClosedGroupUpdate: SwiftProtobuf.Messa
|
|||
if let v = self._name {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
|
||||
}
|
||||
if let v = self._groupID {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
|
||||
if let v = self._groupPublicKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 2)
|
||||
}
|
||||
if let v = self._sharedSecret {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
|
||||
if let v = self._groupPrivateKey {
|
||||
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
|
||||
}
|
||||
if let v = self._senderKey {
|
||||
try visitor.visitSingularStringField(value: v, fieldNumber: 4)
|
||||
if !self.chainKeys.isEmpty {
|
||||
try visitor.visitRepeatedBytesField(value: self.chainKeys, fieldNumber: 4)
|
||||
}
|
||||
if !self.members.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.members, fieldNumber: 5)
|
||||
}
|
||||
if !self.admins.isEmpty {
|
||||
try visitor.visitRepeatedStringField(value: self.admins, fieldNumber: 6)
|
||||
}
|
||||
if let v = self._type {
|
||||
try visitor.visitSingularEnumField(value: v, fieldNumber: 7)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate, rhs: SignalServiceProtos_DataMessage.ClosedGroupUpdate) -> Bool {
|
||||
if lhs._name != rhs._name {return false}
|
||||
if lhs._groupID != rhs._groupID {return false}
|
||||
if lhs._sharedSecret != rhs._sharedSecret {return false}
|
||||
if lhs._senderKey != rhs._senderKey {return false}
|
||||
if lhs._groupPublicKey != rhs._groupPublicKey {return false}
|
||||
if lhs._groupPrivateKey != rhs._groupPrivateKey {return false}
|
||||
if lhs.chainKeys != rhs.chainKeys {return false}
|
||||
if lhs.members != rhs.members {return false}
|
||||
if lhs.admins != rhs.admins {return false}
|
||||
if lhs._type != rhs._type {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_DataMessage.ClosedGroupUpdate.TypeEnum: SwiftProtobuf._ProtoNameProviding {
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
0: .same(proto: "NEW"),
|
||||
]
|
||||
}
|
||||
|
||||
extension SignalServiceProtos_NullMessage: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
|
||||
static let protoMessageName: String = _protobuf_package + ".NullMessage"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
|
@ -4370,7 +4510,7 @@ extension SignalServiceProtos_SyncMessage.OpenGroupDetails: SwiftProtobuf.Messag
|
|||
static let protoMessageName: String = SignalServiceProtos_SyncMessage.protoMessageName + ".OpenGroupDetails"
|
||||
static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
|
||||
1: .same(proto: "url"),
|
||||
2: .same(proto: "channelId"),
|
||||
2: .same(proto: "channelID"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
|
@ -323,7 +323,7 @@ public class TypingIndicatorsImpl: NSObject, TypingIndicators {
|
|||
return
|
||||
}
|
||||
|
||||
if !SessionMetaProtocol.shouldSendTypingIndicator(for: thread) { return }
|
||||
if !SessionMetaProtocol.shouldSendTypingIndicator(in: thread) { return }
|
||||
|
||||
let message = TypingIndicatorMessage(thread: thread, action: action)
|
||||
messageSender.sendPromise(message: message).retainUntilComplete()
|
||||
|
|
Loading…
Reference in New Issue