Finished up the remaining testing and added some more improvements

• Updated the libSession networking to be injected
• Reworked a couple of the cache methods to run via Combine instead of callbacks
• Cleaned up some logic now that the path & status observers are using Combine
• Fixed an issue with the PathVC could render incorrectly
pull/894/head
Morgan Pretty 10 months ago
parent 0002810c7f
commit 86200829e3

@ -286,7 +286,6 @@
C33FD9C4255A54EF00E217F9 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
C33FD9C5255A54EF00E217F9 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */; };
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDADE255A580400E217F9 /* SwiftSingletons.swift */; };
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; };
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; };
C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
@ -1533,7 +1532,6 @@
C33FDA7A255A57FB00E217F9 /* NSRegularExpression+SSK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+SSK.swift"; sourceTree = "<group>"; };
C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = "<group>"; };
C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseDispatchQueue.swift; sourceTree = "<group>"; };
C33FDADE255A580400E217F9 /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = "<group>"; };
C33FDAF1255A580500E217F9 /* ThumbnailService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailService.swift; sourceTree = "<group>"; };
C33FDAFD255A580600E217F9 /* LRUCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LRUCache.swift; sourceTree = "<group>"; };
C33FDB22255A580900E217F9 /* MediaUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaUtils.swift; sourceTree = "<group>"; };
@ -2244,7 +2242,6 @@
FDF848BB29405C5A007DCAE5 /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = "<group>"; };
FDF848E029405D6E007DCAE5 /* SnodeAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPIError.swift; sourceTree = "<group>"; };
FDF848E129405D6E007DCAE5 /* Destination.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Destination.swift; sourceTree = "<group>"; };
FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Network+OnionRequest.swift"; sourceTree = "<group>"; };
FDF848F029406A30007DCAE5 /* Format.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Format.swift; path = "SessionUIKit/Style Guide/Format.swift"; sourceTree = SOURCE_ROOT; };
FDF848F229413DB0007DCAE5 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = "<group>"; };
FDF848F429413EEC007DCAE5 /* SessionCell+Styling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SessionCell+Styling.swift"; sourceTree = "<group>"; };
@ -3442,7 +3439,6 @@
FDF8489929405C5A007DCAE5 /* Models */,
FDF8488F29405C13007DCAE5 /* Types */,
FD2272842C33E28D004D8A6C /* SnodeAPI */,
FDF8489229405C1B007DCAE5 /* Networking */,
FD7F74682BAB8A5D006DDFD8 /* LibSession */,
C3C2A5CD255385F300C340D1 /* Utilities */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
@ -3575,7 +3571,6 @@
C38EF3E4255B6DF4007E1867 /* CommonStrings.swift */,
C38EF304255B6DBE007E1867 /* ImageCache.swift */,
C33FDB8F255A581200E217F9 /* ParamParser.swift */,
C33FDADE255A580400E217F9 /* SwiftSingletons.swift */,
C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */,
C38EF241255B6D67007E1867 /* Collection+OWS.swift */,
C38EF3AE255B6DE5007E1867 /* OrderedDictionary.swift */,
@ -4654,6 +4649,7 @@
FD2272A12C33E336004D8A6C /* BatchResponse.swift */,
FDD383702AFDD0E1001367F2 /* BencodeResponse.swift */,
FD2272952C33E335004D8A6C /* ContentProxy.swift */,
FDF848E129405D6E007DCAE5 /* Destination.swift */,
FD2272A62C33E337004D8A6C /* HTTPHeader.swift */,
FD2272A72C33E337004D8A6C /* HTTPMethod.swift */,
FD22729A2C33E336004D8A6C /* HTTPQueryParam.swift */,
@ -4662,6 +4658,7 @@
FD2272992C33E336004D8A6C /* Network.swift */,
FD22729C2C33E336004D8A6C /* NetworkError.swift */,
FD22729E2C33E336004D8A6C /* PreparedRequest.swift */,
FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */,
FD22729D2C33E336004D8A6C /* ProxiedContentDownloader.swift */,
FD2272A32C33E337004D8A6C /* Request.swift */,
FD2272A52C33E337004D8A6C /* ResponseInfo.swift */,
@ -4672,16 +4669,6 @@
path = Types;
sourceTree = "<group>";
};
FDF8489229405C1B007DCAE5 /* Networking */ = {
isa = PBXGroup;
children = (
FDF848E129405D6E007DCAE5 /* Destination.swift */,
FDB5DAF22A96DD4F002C8721 /* PreparedRequest+Sending.swift */,
FDF848E829405E4E007DCAE5 /* Network+OnionRequest.swift */,
);
path = Networking;
sourceTree = "<group>";
};
FDF8489929405C5A007DCAE5 /* Models */ = {
isa = PBXGroup;
children = (
@ -5837,7 +5824,6 @@
FD2272D52C34ED1E004D8A6C /* OWSViewController.swift in Sources */,
C38EF3B9255B6DE7007E1867 /* ImageEditorPinchGestureRecognizer.swift in Sources */,
FDB3487E2BE856C800B716C2 /* UIBezierPath+Utilities.swift in Sources */,
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
C38EF386255B6DD2007E1867 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */,

@ -185,7 +185,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) {
guard case .answer = mode else {
SessionCallManager.reportFakeCall(info: "Call not in answer mode")
SessionCallManager.reportFakeCall(info: "Call not in answer mode", using: dependencies)
return
}
@ -434,14 +434,16 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
// Register a callback to get the current network status then remove it immediately as we only
// care about the current status
let networkStatusCallbackId: UUID = LibSession.onNetworkStatusChanged { [weak self, dependencies] status in
guard status != .connected else { return }
self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false, using: dependencies) { _ in
self?.tryToReconnect()
}
}
LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId)
dependencies[cache: .libSessionNetwork].networkStatus
.sinkUntilComplete(
receiveValue: { [weak self, dependencies] status in
guard status != .connected else { return }
self?.reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false, using: dependencies) { _ in
self?.tryToReconnect()
}
}
)
let sessionId: String = self.sessionId
let webRTCSession: WebRTCSession = self.webRTCSession

@ -8,6 +8,19 @@ import SessionUIKit
import SessionMessagingKit
import SignalUtilitiesKit
import SessionUtilitiesKit
// MARK: - Cache
public extension Cache {
static let callManager: CacheConfig<CallManagerCacheType, CallManagerImmutableCacheType> = Dependencies.create(
identifier: "callManager",
createInstance: { _ in SessionCallManager.Cache() },
mutableInstance: { $0 },
immutableInstance: { $0 }
)
}
// MARK: - SessionCallManager
public final class SessionCallManager: NSObject, CallManagerProtocol {
let dependencies: Dependencies
@ -29,42 +42,15 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
}
}
private static var _sharedProvider: CXProvider?
static func sharedProvider(useSystemCallLog: Bool) -> CXProvider {
let configuration = buildProviderConfiguration(useSystemCallLog: useSystemCallLog)
if let sharedProvider = self._sharedProvider {
sharedProvider.configuration = configuration
return sharedProvider
}
else {
SwiftSingletons.register(self)
let provider = CXProvider(configuration: configuration)
_sharedProvider = provider
return provider
}
}
static func buildProviderConfiguration(useSystemCallLog: Bool) -> CXProviderConfiguration {
let providerConfiguration = CXProviderConfiguration(localizedName: "Session")
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallGroups = 1
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.generic]
let iconMaskImage = #imageLiteral(resourceName: "SessionGreen32")
providerConfiguration.iconTemplateImageData = iconMaskImage.pngData()
providerConfiguration.includesCallsInRecents = useSystemCallLog
return providerConfiguration
}
// MARK: - Initialization
init(useSystemCallLog: Bool = false, using dependencies: Dependencies) {
self.dependencies = dependencies
if Preferences.isCallKitSupported {
self.provider = SessionCallManager.sharedProvider(useSystemCallLog: useSystemCallLog)
self.provider = dependencies.mutate(cache: .callManager) {
$0.getOrCreateProvider(useSystemCallLog: useSystemCallLog)
}
self.callController = CXCallController()
}
else {
@ -80,9 +66,11 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
// MARK: - Report calls
public static func reportFakeCall(info: String) {
public static func reportFakeCall(info: String, using dependencies: Dependencies) {
let callId = UUID()
let provider = SessionCallManager.sharedProvider(useSystemCallLog: false)
let provider: CXProvider = dependencies.mutate(cache: .callManager) {
$0.getOrCreateProvider(useSystemCallLog: false)
}
provider.reportNewIncomingCall(
with: callId,
update: CXCallUpdate()
@ -121,7 +109,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
callerName: String,
completion: @escaping (Error?) -> Void
) {
let provider = provider ?? Self.sharedProvider(useSystemCallLog: false)
let provider: CXProvider = dependencies.mutate(cache: .callManager) {
$0.getOrCreateProvider(useSystemCallLog: false)
}
// Construct a CXCallUpdate describing the incoming call, including the caller.
let update = CXCallUpdate()
update.localizedCallerName = callerName
@ -213,8 +203,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
if dependencies.hasInitialised(singleton: .appContext) && dependencies[singleton: .appContext].isInBackground {
// Stop all jobs except for message sending and when completed suspend the database
dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] in
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies)
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
}
}
@ -306,3 +296,39 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
MiniCallView.current?.dismiss()
}
}
// MARK: - SessionCallManager Cache
public extension SessionCallManager {
class Cache: CallManagerCacheType {
public var provider: CXProvider?
public func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider {
if let provider: CXProvider = self.provider {
return provider
}
let iconMaskImage: UIImage = #imageLiteral(resourceName: "SessionGreen32")
let configuration = CXProviderConfiguration(localizedName: "Session")
configuration.supportsVideo = true
configuration.maximumCallGroups = 1
configuration.maximumCallsPerCallGroup = 1
configuration.supportedHandleTypes = [.generic]
configuration.iconTemplateImageData = iconMaskImage.pngData()
configuration.includesCallsInRecents = useSystemCallLog
let provider: CXProvider = CXProvider(configuration: configuration)
self.provider = provider
return provider
}
}
}
// MARK: - OGMCacheType
/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way
public protocol CallManagerImmutableCacheType: ImmutableCacheType {}
public protocol CallManagerCacheType: CallManagerImmutableCacheType, MutableCacheType {
func getOrCreateProvider(useSystemCallLog: Bool) -> CXProvider
}

@ -630,7 +630,9 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
DispatchQueue.main.async {
self?.conversationVC?.showInputAccessoryView()
self?.presentingViewController?.dismiss(animated: true, completion: nil)
self?.presentingViewController?.dismiss(animated: true) {
self?.conversationVC?.becomeFirstResponder()
}
}
}
}

@ -488,8 +488,6 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
}
private func updateNavBarButtons(userProfile: Profile) {
if let oldView: ProfilePictureView = navBarProfileView { viewModel.dependencies.removeFeatureObserver(oldView) }
// Profile picture view
let profilePictureView = ProfilePictureView(size: .navigation)
profilePictureView.accessibilityIdentifier = "User settings"
@ -513,12 +511,36 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
// Path status indicator
let pathStatusView = PathStatusView()
let pathStatusView = PathStatusView(using: viewModel.dependencies)
pathStatusView.accessibilityLabel = "Current onion routing path indicator"
viewModel.dependencies.addFeatureObserver(profilePictureView, for: .serviceNetwork) { [weak self] updatedValue, _ in
self?.updateNavBarButtons(userProfile: userProfile)
}
viewModel.dependencies.publisher(feature: .serviceNetwork)
.subscribe(on: DispatchQueue.global(qos: .background), using: viewModel.dependencies)
.receive(on: DispatchQueue.main, using: viewModel.dependencies)
.sink(
receiveCompletion: { [weak self] _ in
/// If the stream completes it means the network cache was reset in which case we want to
/// re-register for updates in the next run loop (as the new cache should be created by then)
DispatchQueue.main.async {
self?.updateNavBarButtons(userProfile: userProfile)
}
},
receiveValue: { [weak profilePictureView, dependencies = viewModel.dependencies] value in
profilePictureView?.update(
publicKey: userProfile.id,
threadVariant: .contact,
displayPictureFilename: nil,
profile: userProfile,
profileIcon: (value == .testnet ?
.letter("T") : // stringlint:disable
.none
),
additionalProfile: nil,
using: dependencies
)
}
)
.store(in: &profilePictureView.disposables)
// Container view
let profilePictureViewContainer = UIView()

@ -42,7 +42,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
var progressiveSearchTimer: Timer?
private var disposables: Set<AnyCancellable> = Set()
private var networkStatusCallbackId: UUID?
// MARK: - Initialization
@ -63,7 +62,6 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
}
deinit {
LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId)
NotificationCenter.default.removeObserver(self)
progressiveSearchTimer?.invalidate()
@ -106,12 +104,13 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
createViews()
networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] _ in
DispatchQueue.main.async {
dependencies[cache: .libSessionNetwork].networkStatus
.receive(on: DispatchQueue.main, using: dependencies)
.sink(receiveValue: { [weak self] _ in
// Prod cells to try to load when connectivity changes.
self?.ensureCellState()
}
}
})
.store(in: &disposables)
NotificationCenter.default.addObserver(
self,

@ -57,8 +57,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
dependencies.set(singleton: .callManager, to: SessionCallManager(using: dependencies))
// Setup LibSession
LibSession.addLogger()
LibSession.createNetworkIfNeeded(using: dependencies)
LibSession.setupLogger(using: dependencies)
dependencies.warmCache(cache: .libSessionNetwork)
// Configure the different targets
SNUtilitiesKit.configure(maxFileSize: Network.maxFileSize, using: dependencies)
@ -164,8 +164,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// Apple's documentation on the matter)
UNUserNotificationCenter.current().delegate = self
Storage.resumeDatabaseAccess(using: dependencies)
LibSession.resumeNetworkAccess()
dependencies[singleton: .storage].resumeDatabaseAccess()
dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() }
// Reset the 'startTime' (since it would be invalid from the last launch)
startTime = CACurrentMediaTime()
@ -229,8 +229,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Stop all jobs except for message sending and when completed suspend the database
dependencies[singleton: .jobRunner].stopAndClearPendingJobs(exceptForVariant: .messageSend) { [dependencies] in
if !self.hasCallOngoing() {
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies)
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.info("[AppDelegate] completed network and database shutdowns.")
Log.flush()
}
@ -300,8 +300,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Log.appResumedExecution()
Log.info("Starting background fetch.")
Storage.resumeDatabaseAccess(using: dependencies)
LibSession.resumeNetworkAccess()
dependencies[singleton: .storage].resumeDatabaseAccess()
dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() }
// Background tasks only last for a certain amount of time (which can result in a crash and a
// prompt appearing for the user), we want to avoid this and need to make sure to suspend the
@ -320,8 +320,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
BackgroundPoller.isValid = false
if dependencies.hasInitialised(singleton: .appContext) && dependencies[singleton: .appContext].isInBackground {
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies)
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
}
@ -350,8 +350,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
BackgroundPoller.isValid = false
if dependencies.hasInitialised(singleton: .appContext) && dependencies[singleton: .appContext].isInBackground {
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies)
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
}

@ -111,8 +111,10 @@ public class SessionApp: SessionAppType {
public func resetData(onReset: (() -> ())) {
homeViewController = nil
LibSession.clearMemoryState(using: dependencies)
LibSession.clearSnodeCache()
LibSession.suspendNetworkAccess()
dependencies.mutate(cache: .libSessionNetwork) {
$0.clearSnodeCache()
$0.suspendNetworkAccess()
}
PushNotificationAPI.deleteKeys(using: dependencies)
Storage.resetAllStorage(using: dependencies)
DisplayPictureManager.resetStorage(using: dependencies)

@ -266,12 +266,12 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate {
let caller: String = payload["caller"] as? String,
let timestampMs: Int64 = payload["timestamp"] as? Int64
else {
SessionCallManager.reportFakeCall(info: "Missing payload data")
SessionCallManager.reportFakeCall(info: "Missing payload data", using: dependencies)
return
}
Storage.resumeDatabaseAccess(using: dependencies)
LibSession.resumeNetworkAccess()
dependencies[singleton: .storage].resumeDatabaseAccess()
dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() }
let maybeCall: SessionCall? = dependencies[singleton: .storage].write { [dependencies] db in
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
@ -318,7 +318,7 @@ public class PushRegistrationManager: NSObject, PKPushRegistryDelegate {
}
guard let call: SessionCall = maybeCall else {
SessionCallManager.reportFakeCall(info: "Could not retrieve call from database")
SessionCallManager.reportFakeCall(info: "Could not retrieve call from database", using: dependencies)
return
}

@ -107,21 +107,18 @@ public enum SyncPushTokensJob: JobExecutor {
Log.info("[SyncPushTokensJob] Re-registering for remote notifications")
dependencies[singleton: .pushRegistrationManager].requestPushTokens()
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<(String, String)?, Error> in
Deferred {
Future<(String, String)?, Error> { resolver in
_ = LibSession.onPathsChanged(skipInitialCallbackIfEmpty: true) { paths, pathsChangedId in
// Only listen for the first callback
LibSession.removePathsChangedCallback(callbackId: pathsChangedId)
guard !paths.isEmpty else {
SNLog("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to lack of paths")
return resolver(Result.success(nil))
}
resolver(Result.success((pushToken, voipToken)))
dependencies[cache: .libSessionNetwork].paths
.first() // Only listen for the first callback
.map { paths in
guard !paths.isEmpty else {
Log.info("[SyncPushTokensJob] OS subscription completed, skipping server subscription due to lack of paths")
return nil
}
return (pushToken, voipToken)
}
}.eraseToAnyPublisher()
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.flatMap { (tokenInfo: (String, String)?) -> AnyPublisher<Void, Error> in
guard let (pushToken, voipToken): (String, String) = tokenInfo else {

@ -65,7 +65,7 @@ extension Onboarding {
public var displayName: String
public var _displayNamePublisher: AnyPublisher<String?, Error>?
private var userProfileConfigMessage: ProcessedMessage?
private var cancelable: AnyCancellable?
private var disposables: Set<AnyCancellable> = Set()
public var displayNamePublisher: AnyPublisher<String?, Error> {
_displayNamePublisher ?? Fail(error: NetworkError.notFound).eraseToAnyPublisher()
@ -164,22 +164,18 @@ extension Onboarding {
self.useAPNS = dependencies[defaults: .standard, key: .isUsingFullAPNs]
self.displayName = displayName
self._displayNamePublisher = nil
self.cancelable = nil
/// Don't trigger the `Identity.didRegister` notification in this case
self.suppressDidRegisterNotification = true
}
deinit {
cancelable?.cancel()
}
// MARK: - Functions
public func setSeedData(_ seedData: Data) throws {
cancelable?.cancel()
/// Reset the disposables in case this was called with different data/
disposables = Set()
// Generate the keys and store them
/// Generate the keys and store them
let identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) = try Identity.generate(
from: seedData,
using: dependencies
@ -265,13 +261,10 @@ extension Onboarding {
/// Store the publisher and cancelable so we only make one request during onboarding
_displayNamePublisher = publisher
cancelable = publisher
publisher
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sink(receiveCompletion: { _ in
print("RAWR")
}, receiveValue: { _ in
print("VDVDFV")
})
.sink(receiveCompletion: { _ in }, receiveValue: { _ in })
.store(in: &disposables)
}
func setUserAPNS(_ useAPNS: Bool) {

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import SessionUIKit
import SessionSnodeKit
import SessionMessagingKit
@ -28,10 +29,12 @@ final class PathStatusView: UIView {
// MARK: - Initialization
private let dependencies: Dependencies
private let size: Size
private var networkStatusCallbackId: UUID?
private var disposables: Set<AnyCancellable> = Set()
init(size: Size = .small) {
init(size: Size = .small, using dependencies: Dependencies) {
self.dependencies = dependencies
self.size = size
super.init(frame: .zero)
@ -41,16 +44,7 @@ final class PathStatusView: UIView {
}
required init?(coder: NSCoder) {
self.size = .small
super.init(coder: coder)
setUpViewHierarchy()
registerObservers()
}
deinit {
LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId)
fatalError("init(coder:) has not been implemented")
}
// MARK: - Layout
@ -65,15 +59,24 @@ final class PathStatusView: UIView {
// MARK: - Functions
private func registerObservers() {
// Register for status updates (will be called immediately with current status)
networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] status in
DispatchQueue.main.async {
self?.setStatus(to: status)
}
}
/// Register for status updates (will be called immediately with current status)
dependencies[cache: .libSessionNetwork].networkStatus
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { [weak self] _ in
/// If the stream completes it means the network cache was reset in which case we want to
/// re-register for updates in the next run loop (as the new cache should be created by then)
DispatchQueue.global(qos: .background).async {
self?.registerObservers()
}
},
receiveValue: { [weak self] status in self?.setStatus(to: status) }
)
.store(in: &disposables)
}
private func setStatus(to status: LibSession.NetworkStatus) {
private func setStatus(to status: NetworkStatus) {
themeBackgroundColor = status.themeColor
layer.themeShadowColor = status.themeColor
layer.shadowOffset = CGSize(width: 0, height: 0.8)
@ -91,7 +94,7 @@ final class PathStatusView: UIView {
}
}
public extension LibSession.NetworkStatus {
public extension NetworkStatus {
var themeColor: ThemeValue {
switch self {
case .unknown: return .path_unknown

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import NVActivityIndicatorView
import SessionMessagingKit
import SessionUIKit
@ -13,9 +14,8 @@ final class PathVC: BaseVC {
private static let rowHeight: CGFloat = (isIPhone5OrSmaller ? 52 : 75)
private let dependencies: Dependencies
private var pathUpdateId: UUID?
private var cacheUpdateId: UUID?
private var lastPath: [LibSession.Snode] = []
private var disposables: Set<AnyCancellable> = Set()
// MARK: - Components
@ -65,11 +65,6 @@ final class PathVC: BaseVC {
fatalError("init(coder:) has not been implemented")
}
deinit {
LibSession.removeNetworkChangedCallback(callbackId: pathUpdateId)
dependencies.mutate(cache: .ip2Country) { $0.removeCacheLoadedCallback(id: cacheUpdateId) }
}
// MARK: - Lifecycle
override func viewDidLoad() {
@ -132,25 +127,41 @@ final class PathVC: BaseVC {
// Set up spacer constraints
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true
// Register for status updates (will be called immediately with current paths)
pathUpdateId = LibSession.onPathsChanged { [weak self] paths, _ in
DispatchQueue.main.async {
self?.update(paths: paths, force: false)
}
}
// Register for path country updates
cacheUpdateId = dependencies.mutate(cache: .ip2Country) { cache in
cache.onCacheLoaded { [weak self] in
DispatchQueue.main.async {
self?.update(paths: (self?.lastPath.map { [$0] } ?? []), force: true)
dependencies[cache: .ip2Country].cacheLoaded
.receive(on: DispatchQueue.main, using: dependencies)
.sink(receiveValue: { [weak self] _ in
switch (self?.lastPath, self?.lastPath.isEmpty == true) {
case (.none, _), (_, true): self?.update(paths: [], force: true)
case (.some(let lastPath), _): self?.update(paths: [lastPath], force: true)
}
}
}
})
.store(in: &disposables)
// Register for network updates
registerNetworkObservables()
}
// MARK: - Updating
private func registerNetworkObservables() {
/// Register for status updates (will be called immediately with current paths)
dependencies[cache: .libSessionNetwork].paths
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { [weak self] _ in
/// If the stream completes it means the network cache was reset in which case we want to
/// re-register for updates in the next run loop (as the new cache should be created by then)
DispatchQueue.global(qos: .background).async {
self?.registerNetworkObservables()
}
},
receiveValue: { [weak self] paths in self?.update(paths: paths, force: false) }
)
.store(in: &disposables)
}
private func update(paths: [[LibSession.Snode]], force: Bool) {
guard let pathToDisplay: [LibSession.Snode] = paths.first else {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
@ -209,7 +220,8 @@ final class PathVC: BaseVC {
let lineView = LineView(
location: location,
dotAnimationStartDelay: dotAnimationStartDelay,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
dotAnimationRepeatInterval: dotAnimationRepeatInterval,
using: dependencies
)
lineView.set(.width, to: PathVC.expandedDotSize)
lineView.set(.height, to: PathVC.rowHeight)
@ -271,7 +283,7 @@ private final class LineView: UIView {
private var dotViewWidthConstraint: NSLayoutConstraint!
private var dotViewHeightConstraint: NSLayoutConstraint!
private var dotViewAnimationTimer: Timer!
private var networkStatusCallbackId: UUID?
private var disposables: Set<AnyCancellable> = Set()
enum Location {
case top, middle, bottom
@ -279,7 +291,7 @@ private final class LineView: UIView {
// MARK: - Initialization
init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) {
init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, using dependencies: Dependencies) {
self.location = location
self.dotAnimationStartDelay = dotAnimationStartDelay
self.dotAnimationRepeatInterval = dotAnimationRepeatInterval
@ -287,7 +299,7 @@ private final class LineView: UIView {
super.init(frame: CGRect.zero)
setUpViewHierarchy()
registerObservers()
registerObservers(using: dependencies)
}
override init(frame: CGRect) {
@ -299,7 +311,6 @@ private final class LineView: UIView {
}
deinit {
LibSession.removeNetworkChangedCallback(callbackId: networkStatusCallbackId)
dotViewAnimationTimer?.invalidate()
}
@ -362,13 +373,21 @@ private final class LineView: UIView {
}
}
private func registerObservers() {
// Register for status updates (will be called immediately with current status)
networkStatusCallbackId = LibSession.onNetworkStatusChanged { [weak self] status in
DispatchQueue.main.async {
self?.setStatus(to: status)
}
}
private func registerObservers(using dependencies: Dependencies) {
/// Register for status updates (will be called immediately with current status)
dependencies[cache: .libSessionNetwork].networkStatus
.receive(on: DispatchQueue.main, using: dependencies)
.sink(
receiveCompletion: { [weak self] _ in
/// If the stream completes it means the network cache was reset in which case we want to
/// re-register for updates in the next run loop (as the new cache should be created by then)
DispatchQueue.global(qos: .background).async {
self?.registerObservers(using: dependencies)
}
},
receiveValue: { [weak self] status in self?.setStatus(to: status) }
)
.store(in: &disposables)
}
private func animate() {
@ -394,7 +413,7 @@ private final class LineView: UIView {
}
}
private func setStatus(to status: LibSession.NetworkStatus) {
private func setStatus(to status: NetworkStatus) {
dotView.themeBackgroundColor = status.themeColor
dotView.layer.themeShadowColor = status.themeColor
}

@ -17,6 +17,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
public let state: TableDataState<Section, TableItem> = TableDataState()
public let observableState: ObservableTableSourceState<Section, TableItem> = ObservableTableSourceState()
private var showAdvancedLogging: Bool = false
private var databaseKeyEncryptionPassword: String = ""
// MARK: - Initialization
@ -29,6 +30,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
public enum Section: SessionTableSection {
case developerMode
case logging
case network
case disappearingMessages
case groups
@ -37,6 +39,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
var title: String? {
switch self {
case .developerMode: return nil
case .logging: return "Logging"
case .network: return "Network"
case .disappearingMessages: return "Disappearing Messages"
case .groups: return "Groups"
@ -52,9 +55,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
}
}
public enum TableItem: Differentiable, CaseIterable {
public enum TableItem: Hashable, Differentiable, CaseIterable {
case developerMode
case defaultLogLevel
case advancedLogging
case loggingCategory(String)
case serviceNetwork
case resetSnodeCache
@ -71,6 +78,70 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case updatedGroupsAllowInviteById
case exportDatabase
// MARK: - Conformance
public typealias DifferenceIdentifier = String
public var differenceIdentifier: String {
switch self {
case .developerMode: return "developerMode"
case .defaultLogLevel: return "defaultLogLevel"
case .advancedLogging: return "advancedLogging"
case .loggingCategory(let categoryIdentifier): return "loggingCategory-\(categoryIdentifier)"
case .serviceNetwork: return "serviceNetwork"
case .resetSnodeCache: return "resetSnodeCache"
case .updatedDisappearingMessages: return "updatedDisappearingMessages"
case .debugDisappearingMessageDurations: return "debugDisappearingMessageDurations"
case .updatedGroups: return "updatedGroups"
case .updatedGroupsDisableAutoApprove: return "updatedGroupsDisableAutoApprove"
case .updatedGroupsRemoveMessagesOnKick: return "updatedGroupsRemoveMessagesOnKick"
case .updatedGroupsAllowHistoricAccessOnInvite: return "updatedGroupsAllowHistoricAccessOnInvite"
case .updatedGroupsAllowDisplayPicture: return "updatedGroupsAllowDisplayPicture"
case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing"
case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions"
case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById"
case .exportDatabase: return "exportDatabase"
}
}
public func isContentEqual(to source: TableItem) -> Bool {
self.differenceIdentifier == source.differenceIdentifier
}
public static var allCases: [TableItem] {
var result: [TableItem] = []
switch TableItem.developerMode {
case .developerMode: result.append(.developerMode); fallthrough
case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough
case .advancedLogging: result.append(.advancedLogging); fallthrough
case .loggingCategory: result.append(.loggingCategory("")); fallthrough
case .serviceNetwork: result.append(.serviceNetwork); fallthrough
case .resetSnodeCache: result.append(.resetSnodeCache); fallthrough
case .updatedDisappearingMessages: result.append(.updatedDisappearingMessages); fallthrough
case .debugDisappearingMessageDurations: result.append(.debugDisappearingMessageDurations); fallthrough
case .updatedGroups: result.append(.updatedGroups); fallthrough
case .updatedGroupsDisableAutoApprove: result.append(.updatedGroupsDisableAutoApprove); fallthrough
case .updatedGroupsRemoveMessagesOnKick: result.append(.updatedGroupsRemoveMessagesOnKick); fallthrough
case .updatedGroupsAllowHistoricAccessOnInvite:
result.append(.updatedGroupsAllowHistoricAccessOnInvite); fallthrough
case .updatedGroupsAllowDisplayPicture: result.append(.updatedGroupsAllowDisplayPicture); fallthrough
case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough
case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough
case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough
case .exportDatabase: result.append(.exportDatabase)
}
return result
}
}
// MARK: - Content
@ -78,6 +149,10 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
private struct State: Equatable {
let developerMode: Bool
let defaultLogLevel: Log.Level
let advancedLogging: Bool
let loggingCategories: [Log.Category: Log.Level]
let serviceNetwork: ServiceNetwork
let debugDisappearingMessageDurations: Bool
@ -96,12 +171,19 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
let title: String = "Developer Settings"
lazy var observation: TargetObservation = ObservationBuilder
.refreshableData(self) { [dependencies] () -> State in
.refreshableData(self) { [weak self, dependencies] () -> State in
State(
developerMode: dependencies[singleton: .storage, key: .developerModeEnabled],
defaultLogLevel: dependencies[feature: .logLevel(cat: .default)],
advancedLogging: (self?.showAdvancedLogging == true),
loggingCategories: dependencies[feature: .allLogLevels].currentValues(using: dependencies),
serviceNetwork: dependencies[feature: .serviceNetwork],
debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations],
updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages],
updatedGroups: dependencies[feature: .updatedGroups],
updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove],
updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick],
@ -141,6 +223,77 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
)
]
),
SectionModel(
model: .logging,
elements: [
SessionCell.Info(
id: .defaultLogLevel,
title: "Default Log Level",
subtitle: """
Sets the default log level
All logging categories which don't have a custom level set below will use this value
""",
trailingAccessory: .dropDown { current.defaultLogLevel.title },
onTap: { [weak self, dependencies] in
self?.transitionToScreen(
SessionTableViewController(
viewModel: SessionListViewModel<Log.Level>(
title: "Default Log Level",
options: Log.Level.allCases.filter { $0 != .default },
behaviour: .autoDismiss(
initialSelection: current.defaultLogLevel,
onOptionSelected: self?.updateDefaulLogLevel
),
using: dependencies
)
)
)
}
),
SessionCell.Info(
id: .advancedLogging,
title: "Advanced Logging",
subtitle: "Show per-category log levels",
trailingAccessory: .toggle(
current.advancedLogging,
oldValue: previous?.advancedLogging
),
onTap: { [weak self] in
self?.setAdvancedLoggingVisibility(to: !current.advancedLogging)
}
)
].appending(
contentsOf: !current.advancedLogging ? nil : current.loggingCategories
.sorted(by: { lhs, rhs in lhs.key.rawValue < rhs.key.rawValue })
.map { category, level in
SessionCell.Info(
id: .loggingCategory(category.rawValue),
title: category.rawValue,
subtitle: "Sets the log level for the \(category.rawValue) category",
trailingAccessory: .dropDown { level.title },
onTap: { [weak self, dependencies] in
self?.transitionToScreen(
SessionTableViewController(
viewModel: SessionListViewModel<Log.Level>(
title: "\(category.rawValue) Log Level",
options: [Log.Level.default] // Move 'default' to the top
.appending(contentsOf: Log.Level.allCases.filter { $0 != .default }),
behaviour: .autoDismiss(
initialSelection: level,
onOptionSelected: { updatedLevel in
self?.updateLogLevel(of: category, to: updatedLevel)
}
),
using: dependencies
)
)
)
}
)
}
)
),
SectionModel(
model: .network,
elements: [
@ -405,9 +558,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
/// then we will get a compile error if it doesn't get resetting instructions added)
TableItem.allCases.forEach { item in
switch item {
case .developerMode: break // Not a feature
case .resetSnodeCache: break // Not a feature
case .exportDatabase: break // Not a feature
case .developerMode: break // Not a feature
case .resetSnodeCache: break // Not a feature
case .exportDatabase: break // Not a feature
case .advancedLogging: break // Not a feature
case .defaultLogLevel: updateDefaulLogLevel(to: nil)
case .loggingCategory: resetLoggingCategories()
case .serviceNetwork: updateServiceNetwork(to: nil)
@ -436,6 +593,31 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
self.dismissScreen(type: .pop)
}
private func updateDefaulLogLevel(to updatedDefaultLogLevel: Log.Level?) {
dependencies.set(feature: .logLevel(cat: .default), to: updatedDefaultLogLevel)
forceRefresh(type: .databaseQuery)
}
private func setAdvancedLoggingVisibility(to value: Bool) {
self.showAdvancedLogging = value
forceRefresh(type: .databaseQuery)
}
private func updateLogLevel(of category: Log.Category, to level: Log.Level) {
switch (level, category.defaultLevel) {
case (.default, category.defaultLevel): dependencies.reset(feature: .logLevel(cat: category))
default: dependencies.set(feature: .logLevel(cat: category), to: level)
}
forceRefresh(type: .databaseQuery)
}
private func resetLoggingCategories() {
dependencies[feature: .allLogLevels].currentValues(using: dependencies).forEach { category, _ in
dependencies.reset(feature: .logLevel(cat: category))
}
forceRefresh(type: .databaseQuery)
}
private func updateServiceNetwork(to updatedNetwork: ServiceNetwork?) {
struct IdentityData {
let ed25519KeyPair: KeyPair
@ -471,19 +653,21 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
})
else { return }
SNLog("[DevSettings] Swapping to \(String(describing: updatedNetwork)), clearing data")
Log.info("[DevSettings] Swapping to \(String(describing: updatedNetwork)), clearing data")
/// Stop all pollers
dependencies[singleton: .currentUserPoller].stop()
dependencies.remove(cache: .groupPollers)
// /// Cancel and remove all current network requests
// dependencies.mutate(cache: .network) { networkCache in
// networkCache.currentRequests.forEach { _, value in value.cancel() }
// networkCache.currentRequests = [:]
// }
/// Reset the network
dependencies.mutate(cache: .libSessionNetwork) {
$0.setPaths(paths: [])
$0.setNetworkStatus(status: .unknown)
}
dependencies.remove(cache: .libSessionNetwork)
/// Unsubscribe from push notifications (do this after cancelling pending network requests as we don't want these to be cancelled)
/// Unsubscribe from push notifications (do this after resetting the network as they are server requests so aren't dependant on a service
/// layer and we don't want these to be cancelled)
if let existingToken: String = dependencies[singleton: .storage, key: .lastRecordedPushToken] {
PushNotificationAPI
.unsubscribeAll(token: Data(hex: existingToken), using: dependencies)
@ -491,7 +675,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
}
/// Clear the snodeAPI caches
dependencies.remove(cache: .snodeAPI) // TODO: Test this works
dependencies.remove(cache: .snodeAPI)
/// Remove any network-specific data
dependencies[singleton: .storage].write { [dependencies] db in
@ -514,14 +698,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
_ = try ConfigDump.deleteAll(db)
}
SNLog("[DevSettings] Reloading state for \(String(describing: updatedNetwork))")
/// Reload the libSession state
/// Remove the libSession state
dependencies.remove(cache: .libSession)
Log.info("[DevSettings] Reloading state for \(String(describing: updatedNetwork))")
/// Update to the new `ServiceNetwork`
dependencies.set(feature: .serviceNetwork, to: updatedNetwork)
/// Start the new network cache
dependencies.warmCache(cache: .libSessionNetwork)
/// Run the onboarding process as if we are recovering an account (will setup the device in it's proper state)
Onboarding.Cache(
ed25519KeyPair: identityData.ed25519KeyPair,
@ -538,7 +725,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
/// Re-sync the push tokens (if there are any)
SyncPushTokensJob.run(uploadOnlyIfStale: false, using: dependencies)
SNLog("[DevSettings] Completed swap to \(String(describing: updatedNetwork))")
Log.info("[DevSettings] Completed swap to \(String(describing: updatedNetwork))")
}
forceRefresh(type: .databaseQuery)
@ -564,7 +751,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
/// Clear the snodeAPI cache
dependencies.remove(cache: .snodeAPI)
/// Clear the snode cache
dependencies.mutate(cache: .libSessionNetwork) { $0.clearSnodeCache() }
}
)
),
@ -706,3 +894,4 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
// MARK: - Listable Conformance
extension ServiceNetwork: Listable {}
extension Log.Level: Listable {}

@ -331,11 +331,11 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
elements: [
SessionCell.Info(
id: .path,
leadingAccessory: .customView(uniqueId: "PathStatusView") { // stringlint:disable
leadingAccessory: .customView(uniqueId: "PathStatusView") { [dependencies] in // stringlint:disable
// Need to ensure this view is the same size as the icons so
// wrap it in a larger view
let result: UIView = UIView()
let pathView: PathStatusView = PathStatusView(size: .large)
let pathView: PathStatusView = PathStatusView(size: .large, using: dependencies)
result.addSubview(pathView)
result.set(.width, to: IconSize.medium.size)

@ -3,6 +3,7 @@
// stringlint:disable
import Foundation
import Combine
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
@ -22,9 +23,11 @@ public extension Cache {
fileprivate class IP2Country: IP2CountryCacheType {
private var countryNamesCache: [String: String] = [:]
private var cacheLoadedCallbacks: [UUID: () -> ()] = [:]
private var pathsChangedCallbackId: UUID? = nil
public var hasFinishedLoading: Bool = false
private let _cacheLoaded: CurrentValueSubject<Bool, Never> = CurrentValueSubject(false)
private var disposables: Set<AnyCancellable> = Set()
public var cacheLoaded: AnyPublisher<Bool, Never> {
_cacheLoaded.filter { $0 }.eraseToAnyPublisher()
}
// MARK: - Tables
@ -60,16 +63,30 @@ fileprivate class IP2Country: IP2CountryCacheType {
Log.info("[IP2Country] Loaded IP country cache.")
/// Then register for path change callbacks which will be used to update the country name cache
self?.pathsChangedCallbackId = LibSession.onPathsChanged(callback: { paths, _ in
/// When a path change occurs we dispatch to the background again to prevent blocking any other path chagne listeners and
/// mutate the cache via dependencies so it blocks other access
DispatchQueue.global(qos: .utility).async { [weak self, dependencies] in
self?.registerNetworkObservables(using: dependencies)
}
}
private func registerNetworkObservables(using dependencies: Dependencies) {
/// Register for path change callbacks which will be used to update the country name cache
dependencies[cache: .libSessionNetwork].paths
.subscribe(on: DispatchQueue.global(qos: .utility), using: dependencies)
.receive(on: DispatchQueue.global(qos: .utility), using: dependencies)
.sink(
receiveCompletion: { [weak self] _ in
/// If the stream completes it means the network cache was reset in which case we want to
/// re-register for updates in the next run loop (as the new cache should be created by then)
DispatchQueue.global(qos: .background).async {
self?.registerNetworkObservables(using: dependencies)
}
},
receiveValue: { [weak self] paths in
dependencies.mutate(cache: .ip2Country) { _ in
self?.populateCacheIfNeeded(paths: paths)
}
}
})
}
)
.store(in: &disposables)
}
private func populateCacheIfNeeded(paths: [[LibSession.Snode]]) {
@ -97,28 +114,16 @@ fileprivate class IP2Country: IP2CountryCacheType {
}
}
self.hasFinishedLoading = true
self._cacheLoaded.send(true)
Log.info("[IP2Country] Update onion request path countries.")
}
// MARK: - Functions
public func onCacheLoaded(callback: @escaping () -> ()) -> UUID {
let id: UUID = UUID()
cacheLoadedCallbacks[id] = callback
return id
}
public func removeCacheLoadedCallback(id: UUID?) {
guard let id: UUID = id else { return }
cacheLoadedCallbacks.removeValue(forKey: id)
}
public func country(for ip: String) -> String {
let fallback: String = "Resolving..."
guard hasFinishedLoading else { return fallback }
guard _cacheLoaded.value else { return fallback }
return (countryNamesCache[ip] ?? fallback)
}
@ -128,15 +133,13 @@ fileprivate class IP2Country: IP2CountryCacheType {
/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way
public protocol IP2CountryImmutableCacheType: ImmutableCacheType {
var hasFinishedLoading: Bool { get }
var cacheLoaded: AnyPublisher<Bool, Never> { get }
func country(for ip: String) -> String
}
public protocol IP2CountryCacheType: IP2CountryImmutableCacheType, MutableCacheType {
var hasFinishedLoading: Bool { get }
var cacheLoaded: AnyPublisher<Bool, Never> { get }
func onCacheLoaded(callback: @escaping () -> ()) -> UUID
func removeCacheLoadedCallback(id: UUID?)
func country(for ip: String) -> String
}

@ -87,6 +87,84 @@ public extension Crypto.Generator {
)
}
}
static func ciphertextForGroupMessage(
groupSessionId: SessionId,
message: [UInt8]
) -> Crypto.Generator<Data> {
return Crypto.Generator(
id: "ciphertextForGroupMessage",
args: [groupSessionId, message]
) { dependencies in
return try dependencies[cache: .libSession]
.config(for: .groupKeys, sessionId: groupSessionId)
.wrappedValue
.map { config in
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
var maybeCiphertext: UnsafeMutablePointer<UInt8>? = nil
var ciphertextLen: Int = 0
groups_keys_encrypt_message(
conf,
message,
message.count,
&maybeCiphertext,
&ciphertextLen
)
guard
ciphertextLen > 0,
let ciphertext: Data = maybeCiphertext
.map({ Data(bytes: $0, count: ciphertextLen) })
else { throw MessageSenderError.encryptionFailed }
return ciphertext
} ?? { throw MessageSenderError.encryptionFailed }()
}
}
static func plaintextForGroupMessage(
groupSessionId: SessionId,
ciphertext: [UInt8]
) throws -> Crypto.Generator<(plaintext: Data, sender: String)> {
return Crypto.Generator(
id: "plaintextForGroupMessage",
args: [groupSessionId, ciphertext]
) { dependencies in
return try dependencies[cache: .libSession]
.config(for: .groupKeys, sessionId: groupSessionId)
.wrappedValue
.map { config -> (Data, String) in
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
var cSessionId: [CChar] = [CChar](repeating: 0, count: 67)
var maybePlaintext: UnsafeMutablePointer<UInt8>? = nil
var plaintextLen: Int = 0
let didDecrypt: Bool = groups_keys_decrypt_message(
conf,
ciphertext,
ciphertext.count,
&cSessionId,
&maybePlaintext,
&plaintextLen
)
// If we got a reported failure then just stop here
guard didDecrypt else { throw MessageReceiverError.decryptionFailed }
// We need to manually free 'maybePlaintext' upon a successful decryption
defer { maybePlaintext?.deallocate() }
guard
plaintextLen > 0,
let plaintext: Data = maybePlaintext
.map({ Data(bytes: $0, count: plaintextLen) })
else { throw MessageReceiverError.decryptionFailed }
return (plaintext, String(cString: cSessionId))
} ?? { throw MessageReceiverError.decryptionFailed }()
}
}
}
public extension Crypto.Verification {

@ -26,10 +26,8 @@ public enum CheckForAppUpdatesJob: JobExecutor {
.tryFlatMap { maybeEd25519SecretKey -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> in
guard let ed25519SecretKey: [UInt8] = maybeEd25519SecretKey else { throw StorageError.objectNotFound }
return LibSession.checkClientVersion(
ed25519SecretKey: ed25519SecretKey,
using: dependencies
)
return dependencies[singleton: .network]
.checkClientVersion(ed25519SecretKey: ed25519SecretKey)
}
.sinkUntilComplete(
receiveCompletion: { _ in

@ -41,7 +41,8 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
.upserted(db)
}
dependencies[singleton: .openGroupManager].getDefaultRoomsIfNeeded()
dependencies[singleton: .openGroupManager]
.getDefaultRoomsIfNeeded()
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(

@ -380,77 +380,6 @@ internal extension LibSession {
})
.defaulting(to: false)
}
static func encrypt(
message: Data,
groupSessionId: SessionId,
using dependencies: Dependencies
) throws -> Data {
return try dependencies[cache: .libSession]
.config(for: .groupKeys, sessionId: groupSessionId)
.wrappedValue
.map { config in
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
var maybeCiphertext: UnsafeMutablePointer<UInt8>? = nil
var ciphertextLen: Int = 0
groups_keys_encrypt_message(
conf,
Array(message),
message.count,
&maybeCiphertext,
&ciphertextLen
)
guard
ciphertextLen > 0,
let ciphertext: Data = maybeCiphertext
.map({ Data(bytes: $0, count: ciphertextLen) })
else { throw MessageSenderError.encryptionFailed }
return ciphertext
} ?? { throw MessageSenderError.encryptionFailed }()
}
static func decrypt(
ciphertext: Data,
groupSessionId: SessionId,
using dependencies: Dependencies
) throws -> (plaintext: Data, sender: String) {
return try dependencies[cache: .libSession]
.config(for: .groupKeys, sessionId: groupSessionId)
.wrappedValue
.map { config -> (Data, String) in
guard case .groupKeys(let conf, _, _) = config else { throw LibSessionError.invalidConfigObject }
var ciphertext: [UInt8] = Array(ciphertext)
var cSessionId: [CChar] = [CChar](repeating: 0, count: 67)
var maybePlaintext: UnsafeMutablePointer<UInt8>? = nil
var plaintextLen: Int = 0
let didDecrypt: Bool = groups_keys_decrypt_message(
conf,
&ciphertext,
ciphertext.count,
&cSessionId,
&maybePlaintext,
&plaintextLen
)
// If we got a reported failure then just stop here
guard didDecrypt else { throw MessageReceiverError.decryptionFailed }
// We need to manually free 'maybePlaintext' upon a successful decryption
defer { maybePlaintext?.deallocate() }
guard
plaintextLen > 0,
let plaintext: Data = maybePlaintext
.map({ Data(bytes: $0, count: plaintextLen) })
else { throw MessageReceiverError.decryptionFailed }
return (plaintext, String(cString: cSessionId))
} ?? { throw MessageReceiverError.decryptionFailed }()
}
}
private extension Int32 {

@ -866,7 +866,6 @@ public enum OpenGroupAPI {
return try Network.PreparedRequest(
request: Request(
method: .post,
endpoint: Endpoint.roomFile(roomToken),
destination: .serverUpload(
server: server,

@ -15,6 +15,17 @@ public extension Singleton {
)
}
// MARK: - Cache
public extension Cache {
static let openGroupManager: CacheConfig<OGMCacheType, OGMImmutableCacheType> = Dependencies.create(
identifier: "openGroupManager",
createInstance: { _ in OpenGroupManager.Cache() },
mutableInstance: { $0 },
immutableInstance: { $0 }
)
}
// MARK: - OpenGroupManager
public final class OpenGroupManager {
@ -1129,15 +1140,6 @@ public extension OpenGroupManager {
}
}
public extension Cache {
static let openGroupManager: CacheConfig<OGMCacheType, OGMImmutableCacheType> = Dependencies.create(
identifier: "openGroupManager",
createInstance: { _ in OpenGroupManager.Cache() },
mutableInstance: { $0 },
immutableInstance: { $0 }
)
}
// MARK: - OGMCacheType
/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way

@ -26,7 +26,6 @@ public extension Request where Endpoint == OpenGroupAPI.Endpoint {
guard let publicKey: String = maybePublicKey else { throw OpenGroupAPIError.noPublicKey }
self = Request(
method: method,
endpoint: endpoint,
destination: try .server(
method: method,

@ -99,10 +99,11 @@ public enum MessageReceiver {
case .groupMessages:
let plaintextEnvelope: Data
(plaintextEnvelope, sender) = try LibSession.decrypt(
ciphertext: data,
groupSessionId: SessionId(.group, hex: publicKey),
using: dependencies
(plaintextEnvelope, sender) = try dependencies[singleton: .crypto].tryGenerate(
.plaintextForGroupMessage(
groupSessionId: SessionId(.group, hex: publicKey),
ciphertext: Array(data)
)
)
guard

@ -175,14 +175,14 @@ public final class MessageSender {
)
.mapError { MessageSenderError.other("Couldn't wrap message", $0) }
.successOrThrow()
// TODO: Crypto?????
return try LibSession
.encrypt(
message: messageData,
let ciphertext: Data = try dependencies[singleton: .crypto].tryGenerate(
.ciphertextForGroupMessage(
groupSessionId: SessionId(.group, hex: groupId),
using: dependencies
message: Array(messageData)
)
.base64EncodedString()
)
return ciphertext.base64EncodedString()
// revokedRetrievableGroupMessages should be sent in plaintext (their content has custom encryption)
case (.closedGroup(let groupId), .revokedRetrievableGroupMessages) where (try? SessionId.Prefix(from: groupId)) == .group:

@ -8,7 +8,7 @@ import SessionUtilitiesKit
public extension Request where Endpoint == PushNotificationAPI.Endpoint {
init(
method: HTTPMethod = .get,
method: HTTPMethod,
endpoint: Endpoint,
queryParameters: [HTTPQueryParam: String] = [:],
headers: [HTTPHeader: String] = [:],
@ -16,7 +16,6 @@ public extension Request where Endpoint == PushNotificationAPI.Endpoint {
using dependencies: Dependencies
) throws {
self = Request(
method: method,
endpoint: endpoint,
destination: try .server(
method: method,

@ -244,7 +244,8 @@ public class Poller: PollerType {
///
/// **Note:** We need a `writePublisher` here because we want to prune the `lastMessageHash` value when preparing
/// the request
return LibSession.getSwarm(for: swarmPublicKey, using: dependencies)
return dependencies[singleton: .network]
.getSwarm(for: swarmPublicKey)
.tryFlatMapWithRandomSnode(drainBehaviour: drainBehaviour, using: dependencies) { [swarmPublicKey, customAuthMethod, dependencies] snode -> AnyPublisher<Network.PreparedRequest<SnodeAPI.PollResponse>, Error> in
dependencies[singleton: .storage].writePublisher { db -> Network.PreparedRequest<SnodeAPI.PollResponse> in
let authMethod: AuthenticationMethod = try (customAuthMethod ?? Authentication.with(

@ -55,7 +55,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let isCallOngoing: Bool = dependencies[defaults: .appGroup, key: .isCallOngoing]
// Perform main setup
Storage.resumeDatabaseAccess(using: dependencies)
dependencies[singleton: .storage].resumeDatabaseAccess()
DispatchQueue.main.sync { self.setUpIfNecessary() { } }
// Handle the push notification
@ -267,8 +267,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
dependencies.set(singleton: .notificationsManager, to: NSENotificationPresenter(using: dependencies))
// Setup LibSession
LibSession.addLogger()
LibSession.createNetworkIfNeeded(using: dependencies)
LibSession.setupLogger(using: dependencies)
dependencies.warmCache(cache: .libSessionNetwork)
// Configure the different targets
SNUtilitiesKit.configure(maxFileSize: Network.maxFileSize, using: dependencies)
@ -364,7 +364,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Log.info("Complete silently.")
if !dependencies[defaults: .appGroup, key: .isMainAppActive] {
Storage.suspendDatabaseAccess(using: dependencies)
dependencies[singleton: .storage].suspendDatabaseAccess()
}
Log.flush()
@ -448,7 +448,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
Log.error("Show generic failure message due to error: \(error).")
if !dependencies[defaults: .appGroup, key: .isMainAppActive] {
Storage.suspendDatabaseAccess(using: dependencies)
dependencies[singleton: .storage].suspendDatabaseAccess()
}
Log.flush()

@ -52,8 +52,8 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
))
// Setup LibSession
LibSession.addLogger()
LibSession.createNetworkIfNeeded(using: dependencies)
LibSession.setupLogger(using: dependencies)
dependencies.warmCache(cache: .libSessionNetwork)
// Configure the different targets
SNUtilitiesKit.configure(maxFileSize: Network.maxFileSize, using: dependencies)
@ -90,12 +90,6 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
name: .sessionDidEnterBackground,
object: nil
)
/// **Note:** If the user opens, dismisses and re-opens the share extension it'll actually use the same instance which
/// results in the `AppSetup` not actually running (and the UI not actually being loaded correctly) - in order to avoid this
/// we call `checkIsAppReady` explicitly here assuming that either the `AppSetup` _hasn't_ complete or won't ever
/// get run
checkIsAppReady(migrationsCompleted: versionMigrationsComplete.wrappedValue)
}
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
@ -127,18 +121,13 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
func checkIsAppReady(migrationsCompleted: Bool) {
Log.assertOnMainThread()
// App isn't ready until storage is ready AND all version migrations are complete.
guard migrationsCompleted else { return }
guard dependencies[singleton: .storage].isValid else {
// If the database is invalid then the UI will handle it
showLockScreenOrMainContent()
return
}
guard !dependencies[singleton: .appReadiness].isAppReady else {
// Only mark the app as ready once.
showLockScreenOrMainContent()
return
}
// If something went wrong during startup then show the UI still (it has custom UI for
// this case) but don't mark the app as ready or trigger the 'launchDidComplete' logic
guard
migrationsCompleted,
dependencies[singleton: .storage].isValid,
!dependencies[singleton: .appReadiness].isAppReady
else { return showLockScreenOrMainContent() }
// Note that this does much more than set a flag;
// it will also run all deferred blocks.
@ -152,10 +141,6 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
super.viewDidLoad()
Log.appResumedExecution()
dependencies[singleton: .appReadiness].runNowOrWhenAppDidBecomeReady { [weak self] in
Log.assertOnMainThread()
self?.showLockScreenOrMainContent()
}
}
@objc

@ -110,8 +110,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
// When the thread picker disappears it means the user has left the screen (this will be called
// whether the user has sent the message or cancelled sending)
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: viewModel.dependencies)
viewModel.dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
viewModel.dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
}
@ -249,14 +249,14 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
shareNavController?.dismiss(animated: true, completion: nil)
ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { [dependencies = viewModel.dependencies] activityIndicator in
Storage.resumeDatabaseAccess(using: dependencies)
LibSession.resumeNetworkAccess()
dependencies[singleton: .storage].resumeDatabaseAccess()
dependencies.mutate(cache: .libSessionNetwork) { $0.resumeNetworkAccess() }
/// When we prepare the message we set the timestamp to be the `dependencies[cache: .snodeAPI].currentOffsetTimestampMs()`
/// but won't actually have a value because the share extension won't have talked to a service node yet which can cause
/// issues with Disappearing Messages, as a result we need to explicitly `getNetworkTime` in order to ensure it's accurate
LibSession
.getSwarm(for: swarmPublicKey, using: dependencies)
dependencies[singleton: .network]
.getSwarm(for: swarmPublicKey)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.tryMapWithRandomSnode(using: dependencies) { snode in
try SnodeAPI.preparedGetNetworkTime(from: snode, using: dependencies)
@ -351,8 +351,8 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { [weak self] result in
LibSession.suspendNetworkAccess()
Storage.suspendDatabaseAccess(using: dependencies)
dependencies.mutate(cache: .libSessionNetwork) { $0.suspendNetworkAccess() }
dependencies[singleton: .storage].suspendDatabaseAccess()
Log.flush()
activityIndicator.dismiss { }

File diff suppressed because it is too large Load Diff

@ -1,65 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import Combine
import SessionUtilitiesKit
public extension Network.RequestType {
static func onionRequest(
_ payload: Data,
to snode: LibSession.Snode,
swarmPublicKey: String?,
timeout: TimeInterval = Network.defaultTimeout
) -> Network.RequestType<Data?> {
return Network.RequestType(
id: "onionRequest",
url: "quic://\(snode.address)",
method: "POST",
body: payload,
args: [payload, snode, swarmPublicKey, timeout]
) { dependencies in
LibSession.sendOnionRequest(
to: Network.Destination.snode(snode),
body: payload,
swarmPublicKey: swarmPublicKey,
timeout: timeout,
using: dependencies
)
}
}
static func onionRequest(
_ request: URLRequest,
to server: String,
with x25519PublicKey: String,
timeout: TimeInterval = Network.defaultTimeout
) -> Network.RequestType<Data?> {
return Network.RequestType(
id: "onionRequest",
url: request.url?.absoluteString,
method: request.httpMethod,
headers: request.allHTTPHeaderFields,
body: request.httpBody,
args: [request, server, x25519PublicKey, timeout]
) { dependencies in
guard let url = request.url, let host = request.url?.host else {
return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()
}
return LibSession.sendOnionRequest(
to: Network.Destination.server(
url: url,
method: (request.httpMethod.map { HTTPMethod(rawValue: $0) } ?? .get),
headers: request.allHTTPHeaderFields,
x25519PublicKey: x25519PublicKey
),
body: request.httpBody,
swarmPublicKey: nil,
timeout: timeout,
using: dependencies
)
}
}
}

@ -4,6 +4,7 @@
import Foundation
import SessionUtilitiesKit
// MARK: Request - SnodeAPI
public extension Request where Endpoint == SnodeAPI.Endpoint {
@ -14,7 +15,6 @@ public extension Request where Endpoint == SnodeAPI.Endpoint {
body: B
) where T == SnodeRequest<B> {
self = Request(
method: .post,
endpoint: endpoint,
destination: .snode(
snode,
@ -34,7 +34,6 @@ public extension Request where Endpoint == SnodeAPI.Endpoint {
snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount
) where T == SnodeRequest<B> {
self = Request(
method: .post,
endpoint: endpoint,
destination: .randomSnode(
swarmPublicKey: swarmPublicKey,
@ -56,7 +55,6 @@ public extension Request where Endpoint == SnodeAPI.Endpoint {
snodeRetrievalRetryCount: Int = SnodeAPI.maxRetryCount
) where T == SnodeRequest<B>, B: Encodable & UpdatableTimestamp {
self = Request(
method: .post,
endpoint: endpoint,
destination: .randomSnodeLatestNetworkTimeTarget(
swarmPublicKey: swarmPublicKey,

@ -277,8 +277,8 @@ public final class SnodeAPI {
// Ask 3 different snodes for the Session ID associated with the given name hash
let base64EncodedNameHash = nameHash.toBase64()
return LibSession
.getRandomNodes(count: validationCount, using: dependencies)
return dependencies[singleton: .network]
.getRandomNodes(count: validationCount)
.tryFlatMap { nodes in
Publishers.MergeMany(
try nodes.map { snode in

@ -31,7 +31,6 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
case onsValidationFailed
// Quic
case invalidNetwork
case invalidPayload
case missingSecretKey
case nodeNotFound(String)
@ -71,7 +70,6 @@ public enum SnodeAPIError: Error, CustomStringConvertible {
case .onsValidationFailed: return "ONS name validation failed (SnodeAPIError.onsValidationFailed)."
// Quic
case .invalidNetwork: return "Unable to create network (SnodeAPIError.invalidNetwork)."
case .invalidPayload: return "Invalid payload (SnodeAPIError.invalidPayload)."
case .missingSecretKey: return "Missing secret key (SnodeAPIError.missingSecretKey)."
case .nodeNotFound(let nodePubkey): return "Next node was not found: \(nodePubkey) (SnodeAPIError.nodeNotFound)."

@ -11,17 +11,28 @@ import SessionUtilitiesKit
public extension Singleton {
static let network: SingletonConfig<NetworkType> = Dependencies.create(
identifier: "network",
createInstance: { dependencies in Network(using: dependencies) }
createInstance: { dependencies in LibSessionNetwork(using: dependencies) }
)
}
// MARK: - NetworkType
public protocol NetworkType {
func send(_ body: Data?, to destination: Network.Destination, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error>
func getSwarm(for swarmPublicKey: String) -> AnyPublisher<Set<LibSession.Snode>, Error>
func getRandomNodes(count: Int) -> AnyPublisher<Set<LibSession.Snode>, Error>
func send(
_ body: Data?,
to destination: Network.Destination,
timeout: TimeInterval
) -> AnyPublisher<(ResponseInfoType, Data?), Error>
func checkClientVersion(ed25519SecretKey: [UInt8]) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error>
}
public class Network: NetworkType {
// MARK: - Network Constants
public class Network {
public static let defaultTimeout: TimeInterval = 10
public static let fileUploadTimeout: TimeInterval = 60
public static let fileDownloadTimeout: TimeInterval = 30
@ -29,87 +40,15 @@ public class Network: NetworkType {
/// **Note:** The max file size is 10,000,000 bytes (rather than 10MiB which would be `(10 * 1024 * 1024)`), 10,000,000
/// exactly will be fine but a single byte more will result in an error
public static let maxFileSize: UInt = 10_000_000
private let dependencies: Dependencies
// MARK: - Initialization
init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
}
// MARK: - RequestType
// MARK: - NetworkStatus
public extension Network {
func send(_ body: Data?, to destination: Destination, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
switch destination {
case .server, .serverUpload, .serverDownload, .cached:
return LibSession.sendRequest(
to: destination,
body: body,
timeout: timeout,
using: dependencies
)
case .snode:
guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() }
return LibSession.sendRequest(
to: destination,
body: body,
timeout: timeout,
using: dependencies
)
case .randomSnode(let swarmPublicKey, let retryCount):
guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() }
return LibSession.getSwarm(for: swarmPublicKey, using: dependencies)
.tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [dependencies] snode in
LibSession.sendRequest(
to: .snode(snode, swarmPublicKey: swarmPublicKey),
body: body,
timeout: timeout,
using: dependencies
)
}
case .randomSnodeLatestNetworkTimeTarget(let swarmPublicKey, let retryCount, let bodyWithUpdatedTimestampMs):
guard body != nil else { return Fail(error: NetworkError.invalidPreparedRequest).eraseToAnyPublisher() }
return LibSession.getSwarm(for: swarmPublicKey, using: dependencies)
.tryFlatMapWithRandomSnode(retry: retryCount, using: dependencies) { [dependencies] snode in
try SnodeAPI
.preparedGetNetworkTime(from: snode, using: dependencies)
.send(using: dependencies)
.tryFlatMap { _, timestampMs in
guard
let updatedEncodable: Encodable = bodyWithUpdatedTimestampMs(timestampMs, dependencies),
let updatedBody: Data = try? JSONEncoder(using: dependencies).encode(updatedEncodable)
else { throw NetworkError.invalidPreparedRequest }
return LibSession
.sendRequest(
to: .snode(snode, swarmPublicKey: swarmPublicKey),
body: updatedBody,
timeout: timeout,
using: dependencies
)
.map { info, response -> (ResponseInfoType, Data?) in
(
SnodeAPI.LatestTimestampResponseInfo(
code: info.code,
headers: info.headers,
timestampMs: timestampMs
),
response
)
}
}
}
}
}
public enum NetworkStatus {
case unknown
case connecting
case connected
case disconnected
}
// MARK: - FileServer Convenience
@ -124,6 +63,7 @@ public extension Network {
public enum Endpoint: EndpointType {
case file
case fileIndividual(String)
case directUrl(URL)
case sessionVersion
public static var name: String { "FileServerAPI.Endpoint" }
@ -132,6 +72,7 @@ public extension Network {
switch self {
case .file: return "file"
case .fileIndividual(let fileId): return "file/\(fileId)"
case .directUrl(let url): return url.path
case .sessionVersion: return "session_version"
}
}
@ -155,7 +96,6 @@ public extension Network {
) throws -> PreparedRequest<FileUploadResponse> {
return try PreparedRequest(
request: Request(
method: .post,
endpoint: FileServer.Endpoint.file,
destination: .serverUpload(
server: FileServer.fileServer,
@ -177,8 +117,7 @@ public extension Network {
) throws -> PreparedRequest<Data> {
return try PreparedRequest(
request: Request<NoBody, FileServer.Endpoint>(
method: .get,
endpoint: FileServer.Endpoint.fileIndividual(""), // TODO: Is this needed????
endpoint: FileServer.Endpoint.directUrl(url),
destination: .serverDownload(
url: url,
x25519PublicKey: FileServer.fileServerPublicKey,

@ -5,6 +5,7 @@
import Foundation
public enum NetworkError: Error, Equatable, CustomStringConvertible {
case invalidState
case invalidURL
case invalidPreparedRequest
case notFound
@ -24,6 +25,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible {
public var description: String {
switch self {
case .invalidState: return "The network is in an invalid state (NetworkError.invalidState)."
case .invalidURL: return "Invalid URL (NetworkError.invalidURL)."
case .invalidPreparedRequest: return "Invalid PreparedRequest provided (NetworkError.invalidPreparedRequest)."
case .notFound: return "Not Found (NetworkError.notFound)."

@ -30,7 +30,6 @@ public extension Network {
public let cancelEventHandler: (() -> Void)?
// The following types are needed for `BatchRequest` handling
public let method: HTTPMethod
private let path: String
public let endpoint: (any EndpointType)
public let endpointName: String
@ -222,7 +221,6 @@ public extension Network {
}()
// The following data is needed in this type for handling batch requests
self.method = request.method
self.endpoint = request.endpoint
self.endpointName = E.name
self.path = request.destination.urlPathAndParamsString
@ -279,7 +277,6 @@ public extension Network {
outputEventHandler: ((CachedResponse) -> Void)?,
completionEventHandler: ((Subscribers.Completion<Error>) -> Void)?,
cancelEventHandler: (() -> Void)?,
method: HTTPMethod,
endpoint: (any EndpointType),
endpointName: String,
headers: [HTTPHeader: String],
@ -309,7 +306,6 @@ public extension Network {
self.cancelEventHandler = cancelEventHandler
// The following data is needed in this type for handling batch requests
self.method = method
self.endpoint = endpoint
self.endpointName = endpointName
self.headers = headers
@ -421,7 +417,7 @@ extension Network.PreparedRequest: ErasedPreparedRequest {
try container.encode(batchRequestHeaders, forKey: .headers)
}
try container.encode(method, forKey: .method)
try container.encode(HTTPMethod.post, forKey: .method) // Should always be POST
try container.encode(path, forKey: .path)
try jsonKeyedBodyEncoder?(&container, .json)
try container.encodeIfPresent(b64, forKey: .b64)
@ -457,7 +453,6 @@ public extension Network.PreparedRequest {
outputEventHandler: outputEventHandler,
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
headers: headers,
@ -524,7 +519,6 @@ public extension Network.PreparedRequest {
},
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
headers: headers,
@ -614,7 +608,6 @@ public extension Network.PreparedRequest {
outputEventHandler: outputEventHandler,
completionEventHandler: completionEventHandler,
cancelEventHandler: cancelEventHandler,
method: method,
endpoint: endpoint,
endpointName: endpointName,
headers: headers,
@ -661,7 +654,6 @@ public extension Network.PreparedRequest {
outputEventHandler: nil,
completionEventHandler: nil,
cancelEventHandler: nil,
method: .get,
endpoint: endpoint,
endpointName: E.name,
headers: [:],

@ -31,7 +31,6 @@ public extension EndpointType {
// MARK: - Request
public struct Request<T: Encodable, Endpoint: EndpointType> {
public let method: HTTPMethod
public let endpoint: Endpoint
public let destination: Network.Destination
public let headers: [HTTPHeader: String]
@ -45,13 +44,11 @@ public struct Request<T: Encodable, Endpoint: EndpointType> {
// MARK: - Initialization
public init(
method: HTTPMethod = .get,
endpoint: Endpoint,
destination: Network.Destination,
headers: [HTTPHeader: String] = [:],
body: T? = nil
) {
self.method = method
self.endpoint = endpoint
self.destination = destination
self.headers = headers
@ -78,9 +75,6 @@ public struct Request<T: Encodable, Endpoint: EndpointType> {
case let bodyDirectData as Data:
return bodyDirectData
case let bodyDirectData as Data:
return bodyDirectData
default:
// Having no body is fine so just return nil
guard let body: T = body else { return nil }

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import YYImage
public final class ProfilePictureView: UIView {
@ -96,6 +97,7 @@ public final class ProfilePictureView: UIView {
}
}
public var disposables: Set<AnyCancellable> = Set()
public var size: Size {
didSet {
widthConstraint.constant = (customWidth ?? size.viewSize)

@ -9,7 +9,7 @@ import Foundation
public extension Singleton {
static let crypto: SingletonConfig<CryptoType> = Dependencies.create(
identifier: "crypto",
createInstance: { _ in Crypto() }
createInstance: { dependencies in Crypto(using: dependencies) }
)
}
@ -36,9 +36,15 @@ public struct Crypto: CryptoType {
public struct Generator<T> {
public let id: String
public let args: [Any?]
fileprivate let generate: () throws -> T
fileprivate let generate: (Dependencies) throws -> T
public init(id: String, args: [Any?] = [], generate: @escaping () throws -> T) {
self.id = id
self.args = args
self.generate = { _ in try generate() }
}
public init(id: String, args: [Any?] = [], generate: @escaping (Dependencies) throws -> T) {
self.id = id
self.args = args
self.generate = generate
@ -48,16 +54,36 @@ public struct Crypto: CryptoType {
public struct Verification {
public let id: String
public let args: [Any?]
let verify: () -> Bool
let verify: (Dependencies) -> Bool
public init(id: String, args: [Any?] = [], verify: @escaping () -> Bool) {
self.id = id
self.args = args
self.verify = { _ in verify() }
}
public init(id: String, args: [Any?] = [], verify: @escaping (Dependencies) -> Bool) {
self.id = id
self.args = args
self.verify = verify
}
}
public init() {}
public func tryGenerate<R>(_ generator: Crypto.Generator<R>) throws -> R { return try generator.generate() }
public func verify(_ verification: Crypto.Verification) -> Bool { return verification.verify() }
// MARK: - Initialization
public let dependencies: Dependencies
public init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
// MARK: - Functions
public func tryGenerate<R>(_ generator: Crypto.Generator<R>) throws -> R {
return try generator.generate(dependencies)
}
public func verify(_ verification: Crypto.Verification) -> Bool {
return verification.verify(dependencies)
}
}

@ -475,17 +475,17 @@ open class Storage {
/// The generally suggested approach is to avoid this entirely by not storing the database in an AppGroup folder and sharing it
/// with extensions - this may be possible but will require significant refactoring and a potentially painful migration to move the
/// database and other files into the App folder
public static func suspendDatabaseAccess(using dependencies: Dependencies) {
public func suspendDatabaseAccess() {
Log.info("[Storage] suspendDatabaseAccess called.")
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
if Storage.hasCreatedValidInstance { dependencies[singleton: .storage].isSuspendedUnsafe = true }
if isValid { isSuspendedUnsafe = true }
}
/// This method reverses the database suspension used to prevent the `0xdead10cc` exception (see `suspendDatabaseAccess()`
/// above for more information
public static func resumeDatabaseAccess(using dependencies: Dependencies) {
public func resumeDatabaseAccess() {
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
if Storage.hasCreatedValidInstance { dependencies[singleton: .storage].isSuspendedUnsafe = false }
if isValid { isSuspendedUnsafe = false }
Log.info("[Storage] resumeDatabaseAccess called.")
}

@ -3,7 +3,7 @@
// stringlint:disable
import Foundation
import GRDB
import Combine
public class Dependencies {
static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")!
@ -13,7 +13,7 @@ public class Dependencies {
private static var cacheInstances: Atomic<[String: Atomic<MutableCacheType>]> = Atomic([:])
private static var userDefaultsInstances: Atomic<[String: (any UserDefaultsType)]> = Atomic([:])
private static var featureInstances: Atomic<[String: (any FeatureType)]> = Atomic([:])
private static var featureObservers: Atomic<[String: [FeatureObservationKey: [((any FeatureOption)?, any FeatureEvent) -> ()]]]> = Atomic([:])
private var featureChangeSubject: PassthroughSubject<(String, String?, Any?), Never> = PassthroughSubject()
// MARK: - Subscript Access
@ -189,65 +189,34 @@ public class Dependencies {
// MARK: - Feature Management
public extension Dependencies {
private struct FeatureObservationKey: Hashable {
let observerHashValue: Int
let events: [(any FeatureEvent)]?
init(_ observer: AnyHashable) {
self.observerHashValue = observer.hashValue
self.events = []
}
init<E: FeatureEvent>(_ observer: AnyHashable, _ events: [E]?) {
self.observerHashValue = observer.hashValue
self.events = events
}
static func == (lhs: Dependencies.FeatureObservationKey, rhs: Dependencies.FeatureObservationKey) -> Bool {
return (lhs.observerHashValue == rhs.observerHashValue)
}
func hash(into hasher: inout Hasher) {
observerHashValue.hash(into: &hasher)
}
func contains<E: FeatureEvent>(event: E) -> Bool {
/// No events mean this observer watches for everything
guard let events: [(any FeatureEvent)] = self.events else { return true }
return events.contains(where: { ($0 as? E) == event })
}
func publisher<T: FeatureOption>(feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
return featureChangeSubject
.filter { identifier, _, _ in identifier == feature.identifier }
.compactMap { _, _, value in value as? T }
.prepend(self[feature: feature]) // Emit the current value first
.eraseToAnyPublisher()
}
func addFeatureObserver<T: FeatureOption>(
_ observer: AnyHashable,
for feature: FeatureConfig<T>,
events: [T.Events]? = nil,
onChange: @escaping (T?, T.Events?) -> ()
) {
Dependencies.featureObservers.mutate {
$0[feature.identifier] = ($0[feature.identifier] ?? [:]).appending(
{ anyUpdate, anyEvent in onChange(anyUpdate as? T, anyEvent as? T.Events) },
toArrayOn: FeatureObservationKey(observer, events)
)
}
func publisher<T: FeatureOption>(featureGroupChanges feature: FeatureConfig<T>) -> AnyPublisher<Void, Never> {
return featureChangeSubject
.filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier }
.map { _, _, _ in () }
.prepend(()) // Emit an initial value to behave similar to the above
.eraseToAnyPublisher()
}
func removeFeatureObserver(_ observer: AnyHashable) {
Dependencies.featureObservers.mutate { featureObservers in
let observationKey: FeatureObservationKey = FeatureObservationKey(observer)
let featureIdentifiers: [String] = Array(featureObservers.keys)
featureIdentifiers.forEach { featureIdentifier in
guard featureObservers[featureIdentifier]?[observationKey] != nil else { return }
featureObservers[featureIdentifier]?.removeValue(forKey: observationKey)
if featureObservers[featureIdentifier]?.isEmpty == true {
featureObservers.removeValue(forKey: featureIdentifier)
}
}
}
func featureUpdated<T: FeatureOption>(for feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
return featureChangeSubject
.filter { identifier, _, _ in identifier == feature.identifier }
.compactMap { _, _, value in value as? T }
.eraseToAnyPublisher()
}
func featureGroupUpdated<T: FeatureOption>(for feature: FeatureConfig<T>) -> AnyPublisher<T?, Never> {
return featureChangeSubject
.filter { _, groupIdentifier, _ in groupIdentifier == feature.groupIdentifier }
.compactMap { _, _, value in value as? T }
.eraseToAnyPublisher()
}
func set<T: FeatureOption>(feature: FeatureConfig<T>, to updatedFeature: T?) {
@ -262,26 +231,23 @@ public extension Dependencies {
}()
value.setValue(to: updatedFeature, using: self)
Dependencies.featureObservers.wrappedValue[feature.identifier]?
.filter { key, _ -> Bool in key.contains(event: T.Events.updateValueEvent) }
.forEach { _, callbacks in callbacks.forEach { $0(updatedFeature, T.Events.updateValueEvent) } }
featureChangeSubject.send((feature.identifier, feature.groupIdentifier, updatedFeature))
}
func notifyObservers<T: FeatureOption>(for feature: FeatureConfig<T>, with event: T.Events) {
let value: Feature<T> = {
guard let value: Feature<T> = (Dependencies.featureInstances.wrappedValue[feature.identifier] as? Feature<T>) else {
let value: Feature<T> = feature.createInstance(self)
Dependencies.featureInstances.mutate { $0[feature.identifier] = value }
return value
}
return value
}()
func reset<T: FeatureOption>(feature: FeatureConfig<T>) {
/// Reset the cached value
switch Dependencies.featureInstances.wrappedValue[feature.identifier] as? Feature<T> {
case .none: break
case .some(let value): value.setValue(to: nil, using: self)
}
/// Reset the in-memory value
Dependencies.featureInstances.mutate {
$0[feature.identifier] = nil
}
let currentFeature: T = value.currentValue(using: self)
Dependencies.featureObservers.wrappedValue[feature.identifier]?
.filter { key, _ -> Bool in key.contains(event: event) }
.forEach { _, callbacks in callbacks.forEach { $0(currentFeature, event) } }
/// Notify observers
featureChangeSubject.send((feature.identifier, feature.groupIdentifier, nil))
}
}

@ -10,15 +10,18 @@ public class FeatureStorage {}
public class FeatureConfig<T: FeatureOption>: FeatureStorage {
public let identifier: String
public let groupIdentifier: String?
public let createInstance: (Dependencies) -> Feature<T>
/// `fileprivate` to hide when accessing via `dependencies[feature: ]`
fileprivate init(
identifier: String,
groupIdentifier: String?,
defaultOption: T,
automaticChangeBehaviour: Feature<T>.ChangeBehaviour?
) {
self.identifier = identifier
self.groupIdentifier = groupIdentifier
self.createInstance = { _ in
Feature<T>(
identifier: identifier,
@ -35,11 +38,13 @@ public class FeatureConfig<T: FeatureOption>: FeatureStorage {
public extension Dependencies {
static func create<T: FeatureOption>(
identifier: String,
groupIdentifier: String? = nil,
defaultOption: T = T.defaultOption,
automaticChangeBehaviour: Feature<T>.ChangeBehaviour? = nil
) -> FeatureConfig<T> {
return FeatureConfig(
identifier: identifier,
groupIdentifier: groupIdentifier,
defaultOption: defaultOption,
automaticChangeBehaviour: automaticChangeBehaviour
)

@ -19,12 +19,6 @@ public enum ServiceNetwork: Int, FeatureOption {
case mainnet = 1
case testnet = 2
public enum Events: FeatureEvent {
case updatedServiceNetwork
public static var updateValueEvent: Events = .updatedServiceNetwork
}
// MARK: - Feature Option
public static var defaultOption: ServiceNetwork = .mainnet

@ -62,8 +62,6 @@ public extension FeatureStorage {
// MARK: - FeatureOption
public protocol FeatureOption: RawRepresentable, CaseIterable, Equatable where RawValue == Int {
associatedtype Events: FeatureEvent
static var defaultOption: Self { get }
var isValidOption: Bool { get }
@ -75,12 +73,6 @@ public extension FeatureOption {
var isValidOption: Bool { true }
}
// MARK: - FeatureEvent
public protocol FeatureEvent: Equatable, Hashable {
static var updateValueEvent: Self { get }
}
// MARK: - FeatureType
public protocol FeatureType {}
@ -210,12 +202,6 @@ public struct FeatureValue<R> {
extension Bool: FeatureOption {
public static let allCases: [Bool] = [false, true]
public enum Events: FeatureEvent {
case updatedFlag
public static var updateValueEvent: Events = .updatedFlag
}
// MARK: - Initialization
public var rawValue: Int { return (self ? 1 : 0) }

@ -5,6 +5,29 @@
import Foundation
import CocoaLumberjackSwift
// MARK: - Log.Level Convenience
public extension Log.Category {
static let `default`: Log.Category = .create("default", defaultLevel: .warn)
}
// MARK: - FeatureStorage
public extension FeatureStorage {
static func logLevel(cat: Log.Category) -> FeatureConfig<Log.Level> {
return Dependencies.create(
identifier: cat.identifier,
groupIdentifier: "logging",
defaultOption: cat.defaultLevel
)
}
static let allLogLevels: FeatureConfig<AllLoggingCategories> = Dependencies.create(
identifier: "allLogLevels",
groupIdentifier: "logging"
)
}
// MARK: - Log
public enum Log {
@ -18,7 +41,7 @@ public enum Log {
line: UInt
)
public enum Level {
public enum Level: CaseIterable {
case verbose
case debug
case info
@ -26,6 +49,43 @@ public enum Log {
case error
case critical
case off
case `default`
}
public struct Category: Hashable {
public let rawValue: String
fileprivate let customPrefix: String
public let defaultLevel: Log.Level
fileprivate static let identifierPrefix: String = "logLevel-"
fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" }
private init(rawValue: String, customPrefix: String, defaultLevel: Log.Level) {
self.rawValue = rawValue
self.customPrefix = customPrefix
self.defaultLevel = defaultLevel
AllLoggingCategories.register(category: self)
}
fileprivate init?(identifier: String) {
guard identifier.hasPrefix(Category.identifierPrefix) else { return nil }
self.init(
rawValue: identifier.substring(from: Category.identifierPrefix.count),
customPrefix: "",
defaultLevel: .default
)
}
public init(rawValue: String, customPrefix: String = "") {
self.init(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: .default)
}
@discardableResult public static func create(_ rawValue: String, customPrefix: String = "", defaultLevel: Log.Level) -> Log.Category {
return Log.Category(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: defaultLevel)
}
}
private static var logger: Atomic<Logger?> = Atomic(nil)
@ -132,7 +192,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.verbose, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.verbose, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func debug(
@ -143,7 +203,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.debug, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.debug, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func info(
@ -154,7 +214,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.info, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.info, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func warn(
@ -165,7 +225,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.warn, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.warn, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func error(
@ -176,7 +236,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.error, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.error, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func critical(
@ -187,7 +247,7 @@ public enum Log {
function: StaticString = #function,
line: UInt = #line
) {
custom(.critical, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
custom(.critical, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
public static func assert(
@ -203,7 +263,7 @@ public enum Log {
let message: String = message()
let logMessage: String = (message.isEmpty ? "Assertion failed." : message)
let formattedMessage: String = "[\(filename):\(line) \(function)] \(logMessage)"
custom(.critical, formattedMessage, withPrefixes: true, silenceForTests: false, file: file, function: function, line: line)
custom(.critical, [], formattedMessage, withPrefixes: true, silenceForTests: false, file: file, function: function, line: line)
assertionFailure(formattedMessage)
}
@ -216,12 +276,13 @@ public enum Log {
let filename: String = URL(fileURLWithPath: "\(file)").lastPathComponent
let formattedMessage: String = "[\(filename):\(line) \(function)] Must be on main thread."
custom(.critical, formattedMessage, withPrefixes: true, silenceForTests: false, file: file, function: function, line: line)
custom(.critical, [], formattedMessage, withPrefixes: true, silenceForTests: false, file: file, function: function, line: line)
assertionFailure(formattedMessage)
}
public static func custom(
_ level: Log.Level,
_ level: Level,
_ categories: [Category],
_ message: String,
withPrefixes: Bool,
silenceForTests: Bool,
@ -238,7 +299,7 @@ public enum Log {
}
}
logger.log(level, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
logger.log(level, categories, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
}
@ -384,7 +445,7 @@ public class Logger {
// If we had an error loading the extension logs then actually log it
if let error: String = error {
Log.empty()
log(.error, error, withPrefixes: true, silenceForTests: false, file: #file, function: #function, line: #line)
log(.error, [], error, withPrefixes: true, silenceForTests: false, file: #file, function: #function, line: #line)
}
// After creating a new logger we want to log two empty lines to make it easier to read
@ -393,12 +454,13 @@ public class Logger {
// Add any logs that were pending during the startup process
pendingLogs.forEach { level, message, withPrefixes, silenceForTests, file, function, line in
log(level, message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
log(level, [], message, withPrefixes: withPrefixes, silenceForTests: silenceForTests, file: file, function: function, line: line)
}
}
fileprivate func log(
_ level: Log.Level,
_ categories: [Log.Category],
_ message: String,
withPrefixes: Bool,
silenceForTests: Bool,
@ -418,6 +480,7 @@ public class Logger {
(DispatchQueue.isDBWriteQueue ? "DBWrite" : nil)
]
.compactMap { $0 }
.appending(contentsOf: categories.map { "\($0.customPrefix)\($0.rawValue)" })
.joined(separator: ", ")
return "[\(prefixes)] "
@ -432,7 +495,7 @@ public class Logger {
.trimmingCharacters(in: .whitespacesAndNewlines)
switch level {
case .off: return
case .off, .default: return
case .verbose: DDLogVerbose("💙 \(logMessage)", file: file, function: function, line: line)
case .debug: DDLogDebug("💚 \(logMessage)", file: file, function: function, line: line)
case .info: DDLogInfo("💛 \(logMessage)", file: file, function: function, line: line)
@ -486,3 +549,106 @@ private extension DispatchQueue {
public func SNLog(_ message: String, forceNSLog: Bool = false) {
Log.info(message)
}
// MARK: - Log.Level FeatureOption
extension Log.Level: FeatureOption {
// MARK: - Initialization
public var rawValue: Int {
switch self {
case .verbose: return 1
case .debug: return 2
case .info: return 3
case .warn: return 4
case .error: return 5
case .critical: return 6
case .off: return -1 // `0` is a protected value so can't use it
case .default: return -2 // `0` is a protected value so can't use it
}
}
public init?(rawValue: Int) {
switch rawValue {
case -2: self = .default // `0` is a protected value so can't use it
case 1: self = .verbose
case 2: self = .debug
case 3: self = .info
case 4: self = .warn
case 5: self = .error
case 6: self = .critical
default: self = .off
}
}
// MARK: - Feature Option
public static var defaultOption: Log.Level = .off
public var title: String {
switch self {
case .verbose: return "Verbose"
case .debug: return "Debug"
case .info: return "Info"
case .warn: return "Warning"
case .error: return "Error"
case .critical: return "Critical"
case .off: return "Off"
case .default: return "Default"
}
}
public var subtitle: String? {
switch self {
case .verbose: return "Show all logging."
case .debug, .info, .warn, .error: return "Show logs classed as \(title) or higher."
case .critical: return "Show logs classes as Critical."
case .off: return "Show no logs."
case .default: return "Use the default logging level."
}
}
}
// MARK: - AllLoggingCategories
public struct AllLoggingCategories: FeatureOption {
public static let allCases: [AllLoggingCategories] = []
private static let registeredCategoryDefaults: Atomic<Set<Log.Category>> = Atomic([])
// MARK: - Initialization
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = -1 // `0` is a protected value so can't use it
}
fileprivate static func register(category: Log.Category) {
guard
!registeredCategoryDefaults.wrappedValue.contains(where: { cat in
/// **Note:** We only want to use the `rawValue` to distinguish between logging categories
/// as the `defaultLevel` can change via the dev settings and any additional metadata could
/// be file/class specific
category.rawValue != cat.rawValue
})
else { return }
registeredCategoryDefaults.mutate { $0.insert(category) }
}
public func currentValues(using dependencies: Dependencies) -> [Log.Category: Log.Level] {
return AllLoggingCategories.registeredCategoryDefaults.wrappedValue
.reduce(into: [:]) { result, cat in
guard cat != Log.Category.default else { return }
result[cat] = dependencies[feature: .logLevel(cat: cat)]
}
}
// MARK: - Feature Option
public static var defaultOption: AllLoggingCategories = AllLoggingCategories(rawValue: -1)
public var title: String = "AllLoggingCategories"
public let subtitle: String? = nil
}

@ -8,28 +8,42 @@ import SessionUtil
// MARK: - LibSession
public enum LibSession {
private static let logLevels: [LogCategory: LOG_LEVEL] = [
.config: LOG_LEVEL_INFO,
.network: LOG_LEVEL_INFO,
.manual: LOG_LEVEL_INFO,
]
public static var version: String { String(cString: LIBSESSION_UTIL_VERSION_STR) }
}
// MARK: - Logging
extension LibSession {
public static func addLogger() {
/// Set the default log level first (unless specified we only care about semi-dangerous logs)
session_logger_set_level_default(LOG_LEVEL_WARN)
public static func setupLogger(using dependencies: Dependencies) {
/// Setup any custom category defaul log levels for libSession
Log.Category.create("config", defaultLevel: .info)
Log.Category.create("network", defaultLevel: .info)
/// Then set any explicit category log levels we have
logLevels.forEach { cat, level in
guard let cCat: [CChar] = cat.rawValue.cString(using: .utf8) else { return }
session_logger_set_level(cCat, level)
}
/// Subscribe for log level changes (this wil' emit an initial event which we can use to set the default log level)
dependencies.publisher(featureGroupChanges: .allLogLevels)
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.sinkUntilComplete(
receiveValue: {
let currentLogLevels: [Log.Category: Log.Level] = dependencies[feature: .allLogLevels]
.currentValues(using: dependencies)
let cDefaultLevel: LOG_LEVEL = (currentLogLevels[.default]?.libSession ?? LOG_LEVEL_OFF)
session_logger_set_level_default(cDefaultLevel)
session_logger_reset_level(cDefaultLevel)
/// Update all explicit log levels (we don't want to register a listener for each individual one so just re-apply all)
///
/// If the conversation to the libSession `LOG_LEVEL` fails then it means we should use the default log level
currentLogLevels.forEach { (category: Log.Category, level: Log.Level) in
guard
let cCat: [CChar] = category.rawValue.cString(using: .utf8),
let cLogLevel: LOG_LEVEL = level.libSession
else { return }
session_logger_set_level(cCat, cLogLevel)
}
}
)
/// Finally register the actual logger callback
session_add_logger_full({ msgPtr, msgLen, catPtr, catLen, lvl in
@ -48,38 +62,37 @@ extension LibSession {
let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines)
return "[libSession:\(cat)] \(logParts[1])] \(message)"
/// Omit the leading square bracket as it'll be removed by the code above
return "\(logParts[1])] \(message)"
}()
Log.custom(
Log.Level(lvl),
[Log.Category(rawValue: cat, customPrefix: "libSession-")],
processedMessage,
withPrefixes: true,
silenceForTests: false
)
})
}
// MARK: - Internal
fileprivate enum LogCategory: String {
case config
case network
case quic
case manual
init?(_ catPtr: UnsafePointer<CChar>?, _ catLen: Int) {
switch String(pointer: catPtr, length: catLen, encoding: .utf8).map({ LogCategory(rawValue: $0) }) {
case .some(let cat): self = cat
case .none: return nil
}
}
}
}
// MARK: - Convenience
fileprivate extension Log.Level {
var libSession: LOG_LEVEL? {
switch self {
case .verbose: return LOG_LEVEL_TRACE
case .debug: return LOG_LEVEL_DEBUG
case .info: return LOG_LEVEL_INFO
case .warn: return LOG_LEVEL_WARN
case .error: return LOG_LEVEL_ERROR
case .critical: return LOG_LEVEL_CRITICAL
case .off: return LOG_LEVEL_OFF
case .default: return nil // It'll use the default value by default so just return nil
}
}
init(_ level: LOG_LEVEL) {
switch level {
case LOG_LEVEL_TRACE: self = .verbose

@ -16,6 +16,8 @@ public extension UserDefaultsStorage {
// MARK: - UserDefaultsType
public protocol UserDefaultsType: AnyObject {
var allKeys: [String] { get }
func object(forKey defaultName: String) -> Any?
func string(forKey defaultName: String) -> String?
func array(forKey defaultName: String) -> [Any]?
@ -45,6 +47,8 @@ extension UserDefaults: UserDefaultsType {}
public extension UserDefaults {
static let applicationGroup: String = "group.com.loki-project.loki-messenger"
var allKeys: [String] { Array(self.dictionaryRepresentation().keys) }
static func removeAll(using dependencies: Dependencies) {
UserDefaultsStorage.standard.createInstance(dependencies).removeAll()
UserDefaultsStorage.appGroup.createInstance(dependencies).removeAll()

@ -1,26 +0,0 @@
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
import Foundation
import SessionUtilitiesKit
public class SwiftSingletons {
public static let shared = SwiftSingletons()
private var classSet = Set<String>()
public func register(_ singleton: AnyObject) {
guard !SNUtilitiesKit.isRunningTests else { return }
guard _isDebugAssertConfiguration() else { return }
let singletonClassName = String(describing: type(of: singleton))
guard !classSet.contains(singletonClassName) else {
Log.error("[SwiftSingletons] Duplicate singleton: \(singletonClassName).")
return
}
Log.verbose("[SwiftSingletons] Registering singleton: \(singletonClassName).")
classSet.insert(singletonClassName)
}
public static func register(_ singleton: AnyObject) {
shared.register(singleton)
}
}
Loading…
Cancel
Save