Further work on Id Blinding

Renamed the setter for the SOGS 'Server' object for consistency
Updated the Curve25519Kit repo to use an Oxen fork
Updated the MockDataGenerator to accomodate the latest changes
Updated the ConversationVC to better support getting replaced when the conversion from blinded to unblinded happens while on that screen
Added a cache for the mapping between blinded ids and standard ids (gets cached whenever a valid match is found)
Added a migration to remove the old 'authToken, 'lastMessageServerId' and 'lastDeletionServerId' collections (redundant in SOGS V4)
This commit is contained in:
Morgan Pretty 2022-03-01 14:06:37 +11:00
parent 3e97782d18
commit a26ee12f8d
24 changed files with 618 additions and 132 deletions

View file

@ -24,8 +24,7 @@ abstract_target 'GlobalDependencies' do
# Dependencies to be included only in all extensions/frameworks # Dependencies to be included only in all extensions/frameworks
abstract_target 'FrameworkAndExtensionDependencies' do abstract_target 'FrameworkAndExtensionDependencies' do
# TODO: Swap this to use an oxen-io fork pod 'Curve25519Kit', git: 'https://github.com/oxen-io/session-ios-curve-25519-kit.git', branch: 'session-version'
pod 'Curve25519Kit', git: 'https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git', branch: 'session'
pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version' pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version'
target 'SessionNotificationServiceExtension' target 'SessionNotificationServiceExtension'

View file

@ -123,7 +123,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- AFNetworking - AFNetworking
- CryptoSwift - CryptoSwift
- Curve25519Kit (from `https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git`, branch `session`) - Curve25519Kit (from `https://github.com/oxen-io/session-ios-curve-25519-kit.git`, branch `session-version`)
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
- Nimble - Nimble
- NVActivityIndicatorView - NVActivityIndicatorView
@ -156,8 +156,8 @@ SPEC REPOS:
EXTERNAL SOURCES: EXTERNAL SOURCES:
Curve25519Kit: Curve25519Kit:
:branch: session :branch: session-version
:git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git :git: https://github.com/oxen-io/session-ios-curve-25519-kit.git
Mantle: Mantle:
:branch: signal-master :branch: signal-master
:git: https://github.com/signalapp/Mantle :git: https://github.com/signalapp/Mantle
@ -175,8 +175,8 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS: CHECKOUT OPTIONS:
Curve25519Kit: Curve25519Kit:
:commit: a23049232dc6c18928cdacfbcef287dad954c5c6 :commit: b79c2ace600bfd3784e9c33cf1f254b121312edc
:git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git :git: https://github.com/oxen-io/session-ios-curve-25519-kit.git
Mantle: Mantle:
:commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4
:git: https://github.com/signalapp/Mantle :git: https://github.com/signalapp/Mantle
@ -214,6 +214,6 @@ SPEC CHECKSUMS:
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 918ef11baf24eac2df681cd6a3781f536f9d384a PODFILE CHECKSUM: 2cc64d50f25c3b1627c3e958ae50e25fead25564
COCOAPODS: 1.11.2 COCOAPODS: 1.11.2

View file

@ -771,6 +771,8 @@
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; };
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; }; FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; }; FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
@ -1906,6 +1908,8 @@
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; }; F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = "<group>"; };
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = "<group>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; }; FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
@ -2564,6 +2568,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B8B32020258B1A650020074B /* Contact.swift */, B8B32020258B1A650020074B /* Contact.swift */,
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */,
); );
path = Contacts; path = Contacts;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3237,6 +3242,7 @@
children = ( children = (
B8B32044258C117C0020074B /* ContactsMigration.swift */, B8B32044258C117C0020074B /* ContactsMigration.swift */,
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */, FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */,
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */, C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */, C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */, C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
@ -4988,6 +4994,7 @@
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */, C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */, C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */, C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */,
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */, FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */, C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */, C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
@ -5117,6 +5124,7 @@
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */, C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */, FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */,
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,

View file

@ -2,6 +2,7 @@ import UIKit
import CoreServices import CoreServices
import Photos import Photos
import PhotosUI import PhotosUI
import Sodium
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
@ -814,6 +815,83 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
present(userDetailsSheet, animated: true, completion: nil) present(userDetailsSheet, animated: true, completion: nil)
} }
func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) {
// If the sessionId is blinded then check if there is an existing un-blinded thread with the contact
if SessionId.Prefix(from: sessionId) == .blinded {
// TODO: Ensure the above case isn't going to be an issue due to legacy messages?
// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard
// sessionId, as a result in order to see if there is an unblinded contact for this blindedId we
// can only really generate blinded ids for each contact and check if any match
//
// Due to this we have made a few optimisations to try and early-out as often as possible, first
// we try to retrieve a direct cached mapping
if let mapping: BlindedIdMapping = Storage.shared.getBlindedIdMapping(with: sessionId) {
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
let conversationVC: ConversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
return
}
var didFindContact: Bool = false
// Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match
ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in
guard Sodium().sessionId(contact.sessionID, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
return
}
// Cache the mapping
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: contact.sessionID, serverPublicKey: openGroupPublicKey)
Storage.shared.cacheBlindedIdMapping(mapping)
// Open the existing thread
let conversationVC: ConversationVC = ConversationVC(thread: contactThread)
self.navigationController?.pushViewController(conversationVC, animated: true)
didFindContact = true
stop.pointee = true
}
// Don't continue if we found the contact
guard !didFindContact else { return }
// Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had
// a thread with this contact in a different SOGS and had cached the mapping)
Storage.shared.enumerateBlindedIdMapping { mapping, stop in
guard mapping.serverPublicKey != openGroupPublicKey else { return }
guard Sodium().sessionId(mapping.sessionId, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
return
}
// Cache the new mapping
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: mapping.sessionId, serverPublicKey: openGroupPublicKey)
Storage.shared.cacheBlindedIdMapping(newMapping)
// Open the existing thread
let conversationVC: ConversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
didFindContact = true
stop.pointee = true
}
// Don't continue if we found the contact
guard !didFindContact else { return }
}
// Just create a new thread with the provided sessionId
let thread = TSContactThread.getOrCreateThread(
contactSessionID: sessionId,
openGroupServer: openGroupServer,
openGroupPublicKey: openGroupPublicKey
)
let conversationVC: ConversationVC = ConversationVC(thread: thread)
self.navigationController?.pushViewController(conversationVC, animated: true)
}
// MARK: Voice Message Playback // MARK: Voice Message Playback
@objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) { @objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) {
// Play the next voice message if there is one // Play the next voice message if there is one

View file

@ -1,3 +1,4 @@
import UIKit
import SessionUIKit import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
@ -13,9 +14,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
let focusedMessageID: String? // This is used for global search let focusedMessageID: String? // This is used for global search
var focusedMessageIndexPath: IndexPath? var focusedMessageIndexPath: IndexPath?
var unreadViewItems: [ConversationViewItem] = [] var unreadViewItems: [ConversationViewItem] = []
var scrollButtonBottomConstraint: NSLayoutConstraint? var isReplacingThread: Bool = false
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
// Search // Search
var isShowingSearchUI = false var isShowingSearchUI = false
var lastSearchedText: String? var lastSearchedText: String?
@ -40,7 +40,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
var audioSession: OWSAudioSession { Environment.shared.audioSession } var audioSession: OWSAudioSession { Environment.shared.audioSession }
var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection } var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems } var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems }
override var canBecomeFirstResponder: Bool { true }
override var canBecomeFirstResponder: Bool {
// Need to return false during the swap between threads to prevent keyboard dismissal
!isReplacingThread
}
override var inputAccessoryView: UIView? { override var inputAccessoryView: UIView? {
if let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup && !thread.isCurrentUserMemberInGroup() { if let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup && !thread.isCurrentUserMemberInGroup() {
@ -102,6 +106,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
private static let messageRequestButtonHeight: CGFloat = 34 private static let messageRequestButtonHeight: CGFloat = 34
var scrollButtonBottomConstraint: NSLayoutConstraint?
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
lazy var titleView: ConversationTitleView = { lazy var titleView: ConversationTitleView = {
let result = ConversationTitleView(thread: thread) let result = ConversationTitleView(thread: thread)
result.delegate = self result.delegate = self
@ -363,6 +371,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil) notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil)
notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleMessageSentStatusChanged), name: .messageSentStatusDidChange, object: nil) notificationCenter.addObserver(self, selector: #selector(handleMessageSentStatusChanged), name: .messageSentStatusDidChange, object: nil)
notificationCenter.addObserver(self, selector: #selector(handleContactThreadReplaced(_:)), name: .contactThreadReplaced, object: nil)
// Mentions // Mentions
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!) MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!)
// Draft // Draft
@ -428,6 +437,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated) super.viewWillDisappear(animated)
// Don't set the draft or resign the first responder if we are replacing the thread (want the keyboard
// to appear to remain focussed)
guard !isReplacingThread else { return }
let text = snInputView.text let text = snInputView.text
Storage.write { transaction in Storage.write { transaction in
self.thread.setDraft(text, transaction: transaction) self.thread.setDraft(text, transaction: transaction)
@ -693,6 +707,90 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
} }
} }
@objc private func handleContactThreadReplaced(_ notification: Notification) {
// Ensure the current thread is one of the removed ones
guard let newThreadId: String = notification.userInfo?[NotificationUserInfoKey.threadId] as? String else { return }
guard let removedThreadIds: [String] = notification.userInfo?[NotificationUserInfoKey.removedThreadIds] as? [String] else {
return
}
guard let threadId: String = thread.uniqueId, removedThreadIds.contains(threadId) else { return }
// Then look to swap the current ConversationVC with a replacement one with the new thread
DispatchQueue.main.async {
guard let navController: UINavigationController = self.navigationController else { return }
guard let viewControllerIndex: Int = navController.viewControllers.firstIndex(of: self) else { return }
guard let newThread: TSContactThread = TSContactThread.fetch(uniqueId: newThreadId) else { return }
// Let the view controller know we are replacing the thread
self.isReplacingThread = true
// Create the new ConversationVC and swap the old one out for it
let conversationVC: ConversationVC = ConversationVC(thread: newThread)
let currentlyOnThisScreen: Bool = (navController.topViewController == self)
navController.viewControllers = [
(viewControllerIndex == 0 ?
[] :
navController.viewControllers[0..<viewControllerIndex]
),
[conversationVC],
(viewControllerIndex == (navController.viewControllers.count - 1) ?
[] :
navController.viewControllers[(viewControllerIndex + 1)..<navController.viewControllers.count]
)
].flatMap { $0 }
// If the top vew controller isn't the current one then we need to make sure to swap out child ones as well
if !currentlyOnThisScreen {
let maybeSettingsViewController: UIViewController? = navController
.viewControllers[viewControllerIndex..<navController.viewControllers.count]
.first(where: { $0 is OWSConversationSettingsViewController })
// Update the settings screen (if there is one)
if let settingsViewController: OWSConversationSettingsViewController = maybeSettingsViewController as? OWSConversationSettingsViewController {
settingsViewController.configure(with: newThread, uiDatabaseConnection: OWSPrimaryStorage.shared().uiDatabaseConnection)
}
}
// Try to minimise painful UX issues by keeping the 'first responder' state, current input text and
// cursor position (Unfortunately there doesn't seem to be a way to prevent the keyboard from
// flickering during the swap but other than that it's relatively seamless)
if self.snInputView.inputTextViewIsFirstResponder {
conversationVC.isReplacingThread = true
conversationVC.snInputView.frame = self.snInputView.frame
conversationVC.snInputView.text = self.snInputView.text
conversationVC.snInputView.selectedRange = self.snInputView.selectedRange
// Make the current snInputView invisible and add the new one the the UI
self.snInputView.alpha = 0
self.snInputView.superview?.addSubview(conversationVC.snInputView)
// Add the old first responder to the window so it the keyboard won't get dismissed when the
// OS removes it's parent view from the view hierarchy due to the view controller swap
var maybeOldFirstResponderView: UIView?
if let oldFirstResponderView: UIView = UIResponder.currentFirstResponder() as? UIView {
maybeOldFirstResponderView = oldFirstResponderView
self.view.window?.addSubview(oldFirstResponderView)
}
// On the next run loop setup the first responder state for the new screen and remove the
// old first responder from the window
DispatchQueue.main.async {
UIView.performWithoutAnimation {
conversationVC.isReplacingThread = false
maybeOldFirstResponderView?.resignFirstResponder()
maybeOldFirstResponderView?.removeFromSuperview()
conversationVC.snInputView.removeFromSuperview()
_ = conversationVC.becomeFirstResponder()
conversationVC.snInputView.inputTextViewBecomeFirstResponder()
}
}
}
}
}
// MARK: General // MARK: General
@objc func addOrRemoveBlockedBanner() { @objc func addOrRemoveBlockedBanner() {
func detach() { func detach() {

View file

@ -24,6 +24,13 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
set { inputTextView.text = newValue } set { inputTextView.text = newValue }
} }
var selectedRange: NSRange {
get { inputTextView.selectedRange }
set { inputTextView.selectedRange = newValue }
}
var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder }
var enabledMessageTypes: MessageTypes = .all { var enabledMessageTypes: MessageTypes = .all {
didSet { didSet {
setEnabledMessageTypes(enabledMessageTypes, message: nil) setEnabledMessageTypes(enabledMessageTypes, message: nil)
@ -337,6 +344,10 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
inputTextView.resignFirstResponder() inputTextView.resignFirstResponder()
} }
func inputTextViewBecomeFirstResponder() {
inputTextView.becomeFirstResponder()
}
func handleLongPress() { func handleLongPress() {
// Not relevant in this case // Not relevant in this case
} }

View file

@ -66,5 +66,5 @@ protocol MessageCellDelegate : AnyObject {
func openURL(_ url: URL) func openURL(_ url: URL)
func handleReplyButtonTapped(for viewItem: ConversationViewItem) func handleReplyButtonTapped(for viewItem: ConversationViewItem)
func showUserDetails(for sessionID: String) func showUserDetails(for sessionID: String)
func startThread(with sessionID: String, openGroupServer: String, openGroupPublicKey: String) func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String)
} }

View file

@ -1,23 +1,24 @@
enum ContactUtilities { enum ContactUtilities {
private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? {
guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil }
guard thread.shouldBeVisible else { return nil }
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else {
return nil
}
guard contact.didApproveMe else { return nil }
return contact
}
static func getAllContacts() -> [String] { static func getAllContacts() -> [String] {
// Collect all contacts // Collect all contacts
var result: [String] = [] var result: [Contact] = []
Storage.read { transaction in Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
guard guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
let thread: TSContactThread = object as? TSContactThread,
thread.shouldBeVisible,
Storage.shared.getContact(
with: thread.contactSessionID(),
using: transaction
)?.didApproveMe == true
else {
return
}
result.append(thread.contactSessionID()) result.append(contact)
} }
} }
func getDisplayName(for publicKey: String) -> String { func getDisplayName(for publicKey: String) -> String {
@ -25,11 +26,24 @@ enum ContactUtilities {
} }
// Remove the current user // Remove the current user
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) { if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) {
result.remove(at: index) result.remove(at: index)
} }
// Sort alphabetically // Sort alphabetically
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) } return result
.map { contact -> String in (contact.displayName(for: .regular) ?? contact.sessionID) }
.sorted()
}
static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer<ObjCBool>) -> ()) {
Storage.read { transaction in
TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in
guard let contactThread: TSContactThread = object as? TSContactThread else { return }
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
block(contactThread, contact, stop)
}
}
} }
} }

View file

@ -189,7 +189,8 @@ enum MockDataGenerator {
image: nil, image: nil,
groupId: groupId, groupId: groupId,
groupType: .closedGroup, groupType: .closedGroup,
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId] adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId],
moderatorIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
) )
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction) let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.shouldBeVisible = true thread.shouldBeVisible = true
@ -232,23 +233,49 @@ enum MockDataGenerator {
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0) let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0) let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let groupDescriptionLength: Int = ((10..<50).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let serverName: String = (0..<serverNameLength) let serverName: String = (0..<serverNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) } .compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined() .joined()
let roomName: String = (0..<roomNameLength) let roomName: String = (0..<roomNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) } .compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined() .joined()
let groupDescription: String = (0..<groupDescriptionLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
// Create the open group model and the thread // Create the open group model and the thread
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil) let openGroup: OpenGroup = OpenGroup(
server: serverName,
room: roomName,
publicKey: randomGroupPublicKey,
name: roomName,
groupDescription: groupDescription,
imageID: nil,
infoUpdates: 0
)
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: []) let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [], moderatorIds: [])
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
thread.shouldBeVisible = true thread.shouldBeVisible = true
thread.save(with: transaction) thread.save(with: transaction)
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) Storage.shared.setOpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
// Generate the 'Server' object
let hasBlinding: Bool = Bool.random(using: &dmThreadRandomGenerator)
let server: OpenGroupAPI.Server = OpenGroupAPI.Server(
name: serverName,
capabilities: OpenGroupAPI.Capabilities(
capabilities: [.sogs]
.appending(hasBlinding ? [.blind] : []),
missing: nil
)
)
Storage.shared.setOpenGroupServer(server, using: transaction)
} }
} }
} }

View file

@ -0,0 +1,40 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@objc(SNBlindedIdMapping)
public class BlindedIdMapping: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
@objc public let blindedId: String
@objc public let sessionId: String
@objc public let serverPublicKey: String
// MARK: - Initialization
@objc public init(blindedId: String, sessionId: String, serverPublicKey: String) {
self.blindedId = blindedId
self.sessionId = sessionId
self.serverPublicKey = serverPublicKey
super.init()
}
private override init() { preconditionFailure("Use init(blindedId:sessionId:) instead.") }
// MARK: - Coding
public required init?(coder: NSCoder) {
guard let blindedId: String = coder.decodeObject(forKey: "blindedId") as! String? else { return nil }
guard let sessionId: String = coder.decodeObject(forKey: "sessionId") as! String? else { return nil }
guard let serverPublicKey: String = coder.decodeObject(forKey: "serverPublicKey") as! String? else { return nil }
self.blindedId = blindedId
self.sessionId = sessionId
self.serverPublicKey = serverPublicKey
}
public func encode(with coder: NSCoder) {
coder.encode(blindedId, forKey: "blindedId")
coder.encode(sessionId, forKey: "sessionId")
coder.encode(serverPublicKey, forKey: "serverPublicKey")
}
}

View file

@ -68,4 +68,44 @@ extension Storage {
} }
return result return result
} }
// MARK: - Blinded Id cache
private static let blindedIdCacheCollection = "BlindedIdCacheCollection"
public func getBlindedIdMapping(with blindedId: String) -> BlindedIdMapping? {
var result: BlindedIdMapping?
Storage.read { transaction in
result = self.getBlindedIdMapping(with: blindedId, using: transaction)
}
return result
}
public func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping? {
return transaction.object(forKey: blindedId, inCollection: Storage.blindedIdCacheCollection) as? BlindedIdMapping
}
public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping) {
Storage.write { transaction in
self.cacheBlindedIdMapping(mapping, using: transaction)
}
}
public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(mapping, forKey: mapping.blindedId, inCollection: Storage.blindedIdCacheCollection)
}
public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> ()) {
Storage.read { transaction in
self.enumerateBlindedIdMapping(with: block, transaction: transaction)
}
}
public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> (), transaction: YapDatabaseReadTransaction) {
transaction.enumerateRows(inCollection: Storage.blindedIdCacheCollection) { _, object, _, stop in
guard let mapping = object as? BlindedIdMapping else { return }
block(mapping, stop)
}
}
} }

View file

@ -2,35 +2,6 @@ import PromiseKit
import Sodium import Sodium
extension Storage { extension Storage {
public func getAllMessageRequestThreads() -> [String: TSContactThread] {
var result: [String: TSContactThread] = [:]
Storage.read { transaction in
result = self.getAllMessageRequestThreads(using: transaction)
}
return result
}
public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] {
var result = [String: TSContactThread]()
// FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15'
let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue)
transaction.enumerateKeysAndObjects(
inCollection: TSContactThread.collection(),
using: { threadID, object, _ in
guard let contactThread = object as? TSContactThread else { return }
result[threadID] = contactThread
},
withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) }
)
return result
}
/// Returns the ID of the thread. /// Returns the ID of the thread.
public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? { public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
let transaction = transaction as! YapDatabaseReadWriteTransaction let transaction = transaction as! YapDatabaseReadWriteTransaction
@ -180,5 +151,35 @@ extension Storage {
let transaction = transaction as! YapDatabaseReadWriteTransaction let transaction = transaction as! YapDatabaseReadWriteTransaction
transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection) transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection)
} }
// MARK: - Message Request Handling
public func getAllMessageRequestThreads() -> [String: TSContactThread] {
var result: [String: TSContactThread] = [:]
Storage.read { transaction in
result = self.getAllMessageRequestThreads(using: transaction)
}
return result
}
public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] {
var result = [String: TSContactThread]()
// FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15'
let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue)
transaction.enumerateKeysAndObjects(
inCollection: TSContactThread.collection(),
using: { threadID, object, _ in
guard let contactThread = object as? TSContactThread else { return }
result[threadID] = contactThread
},
withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) }
)
return result
}
} }

View file

@ -55,36 +55,10 @@ extension Storage {
return result return result
} }
public func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { public func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection) (transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection)
} }
// MARK: - Authorization
private static let authTokenCollection = "SNAuthTokenCollection"
public func getAuthToken(for room: String, on server: String) -> String? {
let collection = Storage.authTokenCollection
let key = "\(server).\(room)"
var result: String? = nil
Storage.read { transaction in
result = transaction.object(forKey: key, inCollection: collection) as? String
}
return result
}
public func setAuthToken(for room: String, on server: String, to newValue: String, using transaction: Any) {
let collection = Storage.authTokenCollection
let key = "\(server).\(room)"
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
}
public func removeAuthToken(for room: String, on server: String, using transaction: Any) {
let collection = Storage.authTokenCollection
let key = "\(server).\(room)"
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
}
// MARK: - Public Keys // MARK: - Public Keys
@ -109,12 +83,12 @@ extension Storage {
// MARK: - Last Message Server ID // MARK: - Open Group Sequence Number
public static let lastMessageServerIDCollection = "SNLastMessageServerIDCollection" public static let openGroupSequenceNumberCollection = "SNOpenGroupSequenceNumberCollection"
public func getLastMessageServerID(for room: String, on server: String) -> Int64? { public func getOpenGroupSequenceNumber(for room: String, on server: String) -> Int64? {
let collection = Storage.lastMessageServerIDCollection let collection = Storage.openGroupSequenceNumberCollection
let key = "\(server).\(room)" let key = "\(server).\(room)"
var result: Int64? = nil var result: Int64? = nil
Storage.read { transaction in Storage.read { transaction in
@ -123,48 +97,41 @@ extension Storage {
return result return result
} }
public func setLastMessageServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) { public func setOpenGroupSequenceNumber(for room: String, on server: String, to newValue: Int64, using transaction: Any) {
let collection = Storage.lastMessageServerIDCollection let collection = Storage.openGroupSequenceNumberCollection
let key = "\(server).\(room)" let key = "\(server).\(room)"
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection) (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
} }
public func removeLastMessageServerID(for room: String, on server: String, using transaction: Any) { public func removeOpenGroupSequenceNumber(for room: String, on server: String, using transaction: Any) {
let collection = Storage.lastMessageServerIDCollection let collection = Storage.openGroupSequenceNumberCollection
let key = "\(server).\(room)" let key = "\(server).\(room)"
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection) (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
} }
// MARK: - -- Open Group Inbox Latest Message Id
public static let openGroupInboxLatestMessageIdCollection = "SNOpenGroupInboxLatestMessageIdCollection"
// MARK: - Last Deletion Server ID public func getOpenGroupInboxLatestMessageId(for server: String) -> Int64? {
let collection = Storage.openGroupInboxLatestMessageIdCollection
public static let lastDeletionServerIDCollection = "SNLastDeletionServerIDCollection"
public func getLastDeletionServerID(for room: String, on server: String) -> Int64? {
let collection = Storage.lastDeletionServerIDCollection
let key = "\(server).\(room)"
var result: Int64? = nil var result: Int64? = nil
Storage.read { transaction in Storage.read { transaction in
result = transaction.object(forKey: key, inCollection: collection) as? Int64 result = transaction.object(forKey: server, inCollection: collection) as? Int64
} }
return result return result
} }
public func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) { public func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) {
let collection = Storage.lastDeletionServerIDCollection let collection = Storage.openGroupInboxLatestMessageIdCollection
let key = "\(server).\(room)" (transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
} }
public func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) { public func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any) {
let collection = Storage.lastDeletionServerIDCollection let collection = Storage.openGroupInboxLatestMessageIdCollection
let key = "\(server).\(room)" (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
} }
// MARK: - Metadata // MARK: - Metadata
private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection" private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"

View file

@ -79,6 +79,10 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value);
- (void)updateTimestamp:(uint64_t)timestamp; - (void)updateTimestamp:(uint64_t)timestamp;
#pragma mark Message Request Thread Migration
- (void)moveToThreadWithId:(NSString *)threadId;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View file

@ -269,6 +269,12 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
} }
#pragma mark - Message Request Thread Migration
- (void)moveToThreadWithId:(NSString *)threadId {
_uniqueThreadId = threadId;
}
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END

View file

@ -35,6 +35,13 @@ extension OpenGroupAPI {
public let capabilities: [Capability] public let capabilities: [Capability]
public let missing: [Capability]? public let missing: [Capability]?
// MARK: - Initialization
public init(capabilities: [Capability], missing: [Capability]? = nil) {
self.capabilities = capabilities
self.missing = missing
}
} }
} }

View file

@ -93,7 +93,6 @@ public final class OpenGroupManager: NSObject {
} }
storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction) storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
let _ = OpenGroupAPI.legacyDeleteAuthToken(for: openGroup.room, on: openGroup.server)
Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction) Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
thread.removeAllThreadInteractions(with: transaction) thread.removeAllThreadInteractions(with: transaction)
@ -119,7 +118,7 @@ public final class OpenGroupManager: NSObject {
capabilities: capabilities capabilities: capabilities
) )
dependencies.storage.storeOpenGroupServer(updatedServer, using: transaction) dependencies.storage.setOpenGroupServer(updatedServer, using: transaction)
} }
} }

View file

@ -1,3 +1,5 @@
import Foundation
import Sodium
import SignalCoreKit import SignalCoreKit
import SessionSnodeKit import SessionSnodeKit
@ -238,8 +240,10 @@ extension MessageReceiver {
thread.remove(with: transaction) thread.remove(with: transaction)
} }
} }
else { else if SessionId.Prefix(from: sessionID) != .blinded {
// Otherwise create and save the thread // Otherwise create and save the thread (if the contact isn't a blinded contact - we don't want to
// auto-create threads for blinded contacts if they have no messages)
// TODO: See what this will do with blinded->unblinded conversations?
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction) let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
thread.shouldBeVisible = true thread.shouldBeVisible = true
thread.save(with: transaction) thread.save(with: transaction)
@ -839,26 +843,130 @@ extension MessageReceiver {
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) { public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
let userPublicKey = getUserHexEncodedPublicKey() let userPublicKey = getUserHexEncodedPublicKey()
var blindedContactIds: [String] = []
var blindedThreadIds: [String] = []
// Ignore messages which were sent from the current user // Ignore messages which were sent from the current user
guard message.sender != userPublicKey else { return } guard message.sender != userPublicKey else { return }
guard let senderId: String = message.sender else { return } guard let senderId: String = message.sender else { return }
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else {
// Get the existing thead and notify the user return
if let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction, let thread: TSContactThread = TSContactThread.getWithContactSessionID(senderId, transaction: transaction) {
let infoMessage = TSInfoMessage(
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
in: thread,
messageType: .messageRequestAccepted
)
infoMessage.save(with: transaction)
} }
// Prep the unblinded thread
let unblindedThreadId: String = TSContactThread.threadID(fromContactSessionID: senderId)
let unblindedThread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: senderId, transaction: transaction)
// Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches
// the blinded ids of any threads)
let messageRequestThreads: [String: TSContactThread] = Storage.shared.getAllMessageRequestThreads(using: transaction)
if !messageRequestThreads.isEmpty {
var interactionsToMove: [TSInteraction] = []
var threadsToDelete: [TSContactThread] = []
// Loop through all blinded threads and extract any interactions relating to the user accepting
// the message request
for blindedThread in messageRequestThreads.values {
let blindedId: String = blindedThread.contactSessionID()
// If the sessionId matches the blindedId then this thread needs to be converted to an un-blinded thread
guard let serverPublicKey: String = blindedThread.originalOpenGroupPublicKey else { continue }
guard Sodium().sessionId(senderId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { continue }
guard let blindedThreadId: String = blindedThread.uniqueId else { continue }
guard let view: YapDatabaseAutoViewTransaction = transaction.ext(TSMessageDatabaseViewExtensionName) as? YapDatabaseAutoViewTransaction else {
continue
}
// Cache the mapping
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: senderId, serverPublicKey: serverPublicKey)
Storage.shared.cacheBlindedIdMapping(mapping, using: transaction)
// Add the `blindedId` to an array so we can remove them at the end of processing
blindedContactIds.append(blindedId)
blindedThreadIds.append(blindedThreadId)
// Loop through all of the interactions and add them to a list to be moved to the new thread
view.enumerateRows(inGroup: blindedThreadId) { _, _, object, _, _, _ in
guard let interaction: TSInteraction = object as? TSInteraction else {
return
}
interactionsToMove.append(interaction)
}
threadsToDelete.append(blindedThread)
// TODO: Pending jobs???
// Storage.shared.getAllPendingJobs(of: <#T##Job.Type#>)
}
// Sort the interactions by their `sortId` (which looks to be a global sort id for all interactions) just in case
// the behaviour changes in the future and the value can get reset (this way we process the interactions in the
// correct order regardless of how many threads they came from)
let sortedInteractionsToMove: [TSInteraction] = interactionsToMove
.sorted { lhs, rhs -> Bool in lhs.sortId < rhs.sortId }
// Note: Unfortunately we need to move the interactions separately from enumerating them to avoid mutating the
// `TSMessageDatabaseViewExtensionName` while enumerating it (this does mean paying the cost of looping a second time)
for interaction in sortedInteractionsToMove {
interaction.moveToThread(withId: unblindedThreadId)
interaction.save(with: transaction)
}
// Delete the old threads
for thread in threadsToDelete {
// TODO: This isn't updating the HomeVC... Race condition??? (Seems to not happen when stepping through with breakpoints)
thread.removeAllThreadInteractions(with: transaction)
thread.remove(with: transaction)
}
}
// Update the `didApproveMe` state of the sender
updateContactApprovalStatusIfNeeded( updateContactApprovalStatusIfNeeded(
senderSessionId: senderId, senderSessionId: senderId,
threadId: nil, threadId: nil,
forceConfigSync: blindedContactIds.isEmpty, // Sync here if there are no blinded contacts
using: transaction
)
// If there were blinded contacts then we should remove them
if !blindedContactIds.isEmpty {
// Delete all of the processed blinded contacts (shouldn't need them anymore and don't want them taking up
// space in the config message)
for blindedId in blindedContactIds {
// TODO: OWSBlockingManager...???
}
// We should assume the 'sender' is a newly created contact and hence need to update it's `isApproved` state
updateContactApprovalStatusIfNeeded(
senderSessionId: userPublicKey,
threadId: unblindedThreadId,
forceConfigSync: true, forceConfigSync: true,
using: transaction using: transaction
) )
} }
// Notify the user of their approval (Note: This will always appear in the un-blinded thread)
// Note: We want to do this last as it'll mean the un-blinded thread gets updated and the contact approval status
// will have been updated at this point (which will mean the `TSThread.isMessageRequest` will return correctly
// after this is saved
let infoMessage = TSInfoMessage(
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
in: unblindedThread,
messageType: .messageRequestAccepted
)
infoMessage.save(with: transaction)
// Finally we need to send a notification that the thread was replaced so we can handle the case where the
// user might currently have the replaced thread open (only need to do this if we actually had blindedIds)
if !blindedThreadIds.isEmpty {
let userInfo: [NotificationUserInfoKey: Any] = [
.threadId: unblindedThreadId,
.removedThreadIds: blindedThreadIds
]
NotificationCenter.default.post(name: .contactThreadReplaced, object: nil, userInfo: userInfo)
}
}
} }

View file

@ -60,7 +60,7 @@ public protocol SessionMessagingKitStorageProtocol {
func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any) func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any)
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? func getOpenGroupServer(name: String) -> OpenGroupAPI.Server?
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any)
// MARK: - -- Open Group Public Keys // MARK: - -- Open Group Public Keys

View file

@ -4,6 +4,7 @@ public extension Notification.Name {
static let groupThreadUpdated = Notification.Name("groupThreadUpdated") static let groupThreadUpdated = Notification.Name("groupThreadUpdated")
static let muteSettingUpdated = Notification.Name("muteSettingUpdated") static let muteSettingUpdated = Notification.Name("muteSettingUpdated")
static let messageSentStatusDidChange = Notification.Name("messageSentStatusDidChange") static let messageSentStatusDidChange = Notification.Name("messageSentStatusDidChange")
static let contactThreadReplaced = Notification.Name("contactThreadReplaced")
} }
@objc public extension NSNotification { @objc public extension NSNotification {
@ -12,3 +13,8 @@ public extension Notification.Name {
@objc static let muteSettingUpdated = Notification.Name.muteSettingUpdated.rawValue as NSString @objc static let muteSettingUpdated = Notification.Name.muteSettingUpdated.rawValue as NSString
@objc static let messageSentStatusDidChange = Notification.Name.messageSentStatusDidChange.rawValue as NSString @objc static let messageSentStatusDidChange = Notification.Name.messageSentStatusDidChange.rawValue as NSString
} }
public enum NotificationUserInfoKey: String {
case threadId
case removedThreadIds
}

View file

@ -10,13 +10,24 @@ extern NSString *const TSContactThreadPrefix;
@interface TSContactThread : TSThread @interface TSContactThread : TSThread
@property (nonatomic, nullable) NSString *originalOpenGroupServer;
@property (nonatomic, nullable) NSString *originalOpenGroupPublicKey;
- (instancetype)initWithContactSessionID:(NSString *)contactSessionID; - (instancetype)initWithContactSessionID:(NSString *)contactSessionID;
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID NS_SWIFT_NAME(getOrCreateThread(contactSessionID:)); + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID NS_SWIFT_NAME(getOrCreateThread(contactSessionID:));
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
openGroupServer:(NSString *)openGroupServer
openGroupPublicKey:(NSString *)openGroupPublicKey NS_SWIFT_NAME(getOrCreateThread(contactSessionID:openGroupServer:openGroupPublicKey:));
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
transaction:(YapDatabaseReadWriteTransaction *)transaction; transaction:(YapDatabaseReadWriteTransaction *)transaction;
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
openGroupServer:(NSString *)openGroupServer
openGroupPublicKey:(NSString *)openGroupPublicKey
transaction:(YapDatabaseReadWriteTransaction *)transaction;
// Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist. // Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist.
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction; + (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;

View file

@ -33,6 +33,23 @@ NSString *const TSContactThreadPrefix = @"c";
return thread; return thread;
} }
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
openGroupServer:(NSString *)openGroupServer
openGroupPublicKey:(NSString *)openGroupPublicKey
transaction:(YapDatabaseReadWriteTransaction *)transaction
{
TSContactThread *thread = [self fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction];
if (!thread) {
thread = [[TSContactThread alloc] initWithContactSessionID:contactSessionID];
thread.originalOpenGroupServer = openGroupServer;
thread.originalOpenGroupPublicKey = openGroupPublicKey;
[thread saveWithTransaction:transaction];
}
return thread;
}
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID + (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
{ {
__block TSContactThread *thread; __block TSContactThread *thread;
@ -43,6 +60,18 @@ NSString *const TSContactThreadPrefix = @"c";
return thread; return thread;
} }
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
openGroupServer:(NSString *)openGroupServer
openGroupPublicKey:(NSString *)openGroupPublicKey
{
__block TSContactThread *thread;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
thread = [self getOrCreateThreadWithContactSessionID:contactSessionID openGroupServer:openGroupServer openGroupPublicKey:openGroupPublicKey transaction:transaction];
}];
return thread;
}
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction; + (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;
{ {
return [TSContactThread fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction]; return [TSContactThread fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction];

View file

@ -86,7 +86,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) } func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) }
func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup } func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup }
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server } func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server }
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server } func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server }
func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? { func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? {
return (mockData[.openGroupUserCount] as? UInt64) return (mockData[.openGroupUserCount] as? UInt64)

View file

@ -0,0 +1,33 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@objc(SNSOGSV4Migration)
public class SOGSV4Migration: OWSDatabaseMigration {
@objc
class func migrationId() -> String {
return "003"
}
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
self.doMigrationAsync(completion: completion)
}
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
// These collections became redundant in SOGS V4
let lastMessageServerIDCollection: String = "SNLastMessageServerIDCollection"
let lastDeletionServerIDCollection: String = "SNLastDeletionServerIDCollection"
let authTokenCollection: String = "SNAuthTokenCollection"
Storage.write(with: { transaction in
transaction.removeAllObjects(inCollection: lastMessageServerIDCollection)
transaction.removeAllObjects(inCollection: lastDeletionServerIDCollection)
transaction.removeAllObjects(inCollection: authTokenCollection)
self.save(with: transaction) // Intentionally capture self
}, completion: {
completion()
})
}
}