mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
887 lines
38 KiB
Swift
887 lines
38 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import SessionSnodeKit
|
|
import SessionUtil
|
|
import SessionUtilitiesKit
|
|
|
|
// MARK: - Cache
|
|
|
|
public extension Cache {
|
|
static let libSession: CacheConfig<LibSessionCacheType, LibSessionImmutableCacheType> = Dependencies.create(
|
|
identifier: "libSession",
|
|
createInstance: { dependencies in NoopLibSessionCache(using: dependencies) },
|
|
mutableInstance: { $0 },
|
|
immutableInstance: { $0 }
|
|
)
|
|
}
|
|
|
|
// MARK: - LibSession
|
|
|
|
public extension LibSession {
|
|
internal static func syncDedupeId(_ swarmPublicKey: String) -> String {
|
|
return "EnqueueConfigurationSyncJob-\(swarmPublicKey)" // stringlint:disable
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
public extension LibSession {
|
|
static func parseCommunity(url: String) -> (room: String, server: String, publicKey: String)? {
|
|
var cBaseUrl: [CChar] = [CChar](repeating: 0, count: COMMUNITY_BASE_URL_MAX_LENGTH)
|
|
var cRoom: [CChar] = [CChar](repeating: 0, count: COMMUNITY_ROOM_MAX_LENGTH)
|
|
var cPubkey: [UInt8] = [UInt8](repeating: 0, count: OpenGroup.pubkeyByteLength)
|
|
|
|
guard
|
|
var cFullUrl: [CChar] = url.cString(using: .utf8),
|
|
community_parse_full_url(&cFullUrl, &cBaseUrl, &cRoom, &cPubkey) &&
|
|
!String(cString: cRoom).isEmpty &&
|
|
!String(cString: cBaseUrl).isEmpty &&
|
|
cPubkey.contains(where: { $0 != 0 })
|
|
else { return nil }
|
|
|
|
// Note: Need to store them in variables instead of returning directly to ensure they
|
|
// don't get freed from memory early (was seeing this happen intermittently during
|
|
// unit tests...)
|
|
let room: String = String(cString: cRoom)
|
|
let baseUrl: String = String(cString: cBaseUrl)
|
|
let pubkeyHex: String = Data(cPubkey).toHexString()
|
|
|
|
return (room, baseUrl, pubkeyHex)
|
|
}
|
|
|
|
static func communityUrlFor(server: String?, roomToken: String?, publicKey: String?) -> String? {
|
|
guard
|
|
var cBaseUrl: [CChar] = server?.cString(using: .utf8),
|
|
var cRoom: [CChar] = roomToken?.cString(using: .utf8),
|
|
let publicKey: String = publicKey
|
|
else { return nil }
|
|
|
|
var cPubkey: [UInt8] = Array(Data(hex: publicKey))
|
|
var cFullUrl: [CChar] = [CChar](repeating: 0, count: COMMUNITY_FULL_URL_MAX_LENGTH)
|
|
community_make_full_url(&cBaseUrl, &cRoom, &cPubkey, &cFullUrl)
|
|
|
|
return String(cString: cFullUrl)
|
|
}
|
|
}
|
|
|
|
// MARK: - ConfigStore
|
|
|
|
private class ConfigStore {
|
|
private struct Key: Hashable {
|
|
let sessionId: SessionId
|
|
let variant: ConfigDump.Variant
|
|
|
|
init(sessionId: SessionId, variant: ConfigDump.Variant) {
|
|
self.sessionId = sessionId
|
|
self.variant = variant
|
|
}
|
|
}
|
|
|
|
private var store: [Key: LibSession.Config] = [:]
|
|
public var isEmpty: Bool { store.isEmpty }
|
|
public var needsSync: Bool { store.contains { _, config in config.needsPush } }
|
|
|
|
subscript (sessionId: SessionId, variant: ConfigDump.Variant) -> LibSession.Config? {
|
|
get { return (store[Key(sessionId: sessionId, variant: variant)] ?? nil) }
|
|
set { store[Key(sessionId: sessionId, variant: variant)] = newValue }
|
|
}
|
|
|
|
subscript (sessionId: SessionId) -> [LibSession.Config] {
|
|
get { return ConfigDump.Variant.allCases.compactMap { store[Key(sessionId: sessionId, variant: $0)] } }
|
|
}
|
|
|
|
deinit {
|
|
/// Group configs are a little complicated because they contain the info & members confs so we need some special handling to
|
|
/// properly free memory here, firstly we need to retrieve all groupKeys configs
|
|
let groupKeysConfigs: [(key: Key, value: LibSession.Config)] = store
|
|
.filter { _, config in
|
|
switch config {
|
|
case .groupKeys: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
/// Now we remove all configss associated to the same sessionId from the store
|
|
groupKeysConfigs.forEach { key, _ in
|
|
ConfigDump.Variant.allCases.forEach { store.removeValue(forKey: Key(sessionId: key.sessionId, variant: $0)) }
|
|
}
|
|
|
|
/// Then free the group configs
|
|
groupKeysConfigs.forEach { _, config in
|
|
switch config {
|
|
case .groupKeys(let keysConf, let infoConf, let membersConf):
|
|
groups_keys_free(keysConf)
|
|
config_free(infoConf)
|
|
config_free(membersConf)
|
|
|
|
default: break
|
|
}
|
|
}
|
|
|
|
/// Finally we free any remaining configs
|
|
store.forEach { _, config in
|
|
switch config {
|
|
case .invalid, .groupKeys: break // Shouldn't happen
|
|
case .object(let conf): config_free(conf)
|
|
}
|
|
}
|
|
store.removeAll()
|
|
}
|
|
}
|
|
|
|
// MARK: - SessionUtil Cache
|
|
|
|
public extension LibSession {
|
|
class Cache: LibSessionCacheType {
|
|
private var configStore: ConfigStore = ConfigStore()
|
|
|
|
public let dependencies: Dependencies
|
|
public let userSessionId: SessionId
|
|
public var isEmpty: Bool { configStore.isEmpty }
|
|
|
|
/// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been
|
|
/// loaded yet (eg. fresh install)
|
|
public var needsSync: Bool { configStore.needsSync }
|
|
|
|
// MARK: - Initialization
|
|
|
|
public init(userSessionId: SessionId, using dependencies: Dependencies) {
|
|
self.userSessionId = userSessionId
|
|
self.dependencies = dependencies
|
|
}
|
|
|
|
// MARK: - State Management
|
|
|
|
public func loadState(_ db: Database) {
|
|
// Ensure we have the ed25519 key and that we haven't already loaded the state before
|
|
// we continue
|
|
guard
|
|
configStore.isEmpty,
|
|
let ed25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db)
|
|
else { return Log.warn(.libSession, "Ignoring loadState due to existing state") }
|
|
|
|
// Retrieve the existing dumps from the database
|
|
let existingDumps: [ConfigDump] = ((try? ConfigDump.fetchSet(db)) ?? [])
|
|
.sorted { lhs, rhs in lhs.variant.loadOrder < rhs.variant.loadOrder }
|
|
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
|
|
.map { $0.variant }
|
|
.asSet()
|
|
let missingRequiredVariants: Set<ConfigDump.Variant> = ConfigDump.Variant.userVariants
|
|
.subtracting(existingDumpVariants)
|
|
let groupsByKey: [String: ClosedGroup] = (try? ClosedGroup
|
|
.filter(ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
|
|
.fetchAll(db)
|
|
.reduce(into: [:]) { result, next in result[next.threadId] = next })
|
|
.defaulting(to: [:])
|
|
let groupsWithNoDumps: [ClosedGroup] = groupsByKey
|
|
.values
|
|
.filter { group in !existingDumps.contains(where: { $0.sessionId.hexString == group.id }) }
|
|
|
|
// Create the config records for each dump
|
|
existingDumps.forEach { dump in
|
|
configStore[dump.sessionId, dump.variant] = try? loadState(
|
|
for: dump.variant,
|
|
sessionId: dump.sessionId,
|
|
userEd25519SecretKey: ed25519KeyPair.secretKey,
|
|
groupEd25519SecretKey: groupsByKey[dump.sessionId.hexString]?
|
|
.groupIdentityPrivateKey
|
|
.map { Array($0) },
|
|
cachedData: dump.data
|
|
)
|
|
}
|
|
|
|
/// It's possible for there to not be dumps for all of the user configs so we load any missing ones to ensure funcitonality
|
|
/// works smoothly
|
|
///
|
|
/// It's also possible for a group to get created but for a dump to not be created (eg. when a crash happens at the right time), to
|
|
/// handle this we also load the state of any groups which don't have dumps if they aren't in the `invited` state (those in
|
|
/// the `invited` state will have their state loaded if the invite is accepted)
|
|
loadDefaultStatesFor(
|
|
userConfigVariants: missingRequiredVariants,
|
|
groups: groupsWithNoDumps,
|
|
userSessionId: userSessionId,
|
|
userEd25519KeyPair: ed25519KeyPair
|
|
)
|
|
Log.info(.libSession, "Completed loadState")
|
|
}
|
|
|
|
public func loadDefaultStatesFor(
|
|
userConfigVariants: Set<ConfigDump.Variant>,
|
|
groups: [ClosedGroup],
|
|
userSessionId: SessionId,
|
|
userEd25519KeyPair: KeyPair
|
|
) {
|
|
/// Create an empty state for the specified user config variants
|
|
userConfigVariants.forEach { variant in
|
|
configStore[userSessionId, variant] = try? loadState(
|
|
for: variant,
|
|
sessionId: userSessionId,
|
|
userEd25519SecretKey: userEd25519KeyPair.secretKey,
|
|
groupEd25519SecretKey: nil,
|
|
cachedData: nil
|
|
)
|
|
}
|
|
|
|
/// Create empty group states for the provided groups
|
|
groups
|
|
.filter { $0.invited != true }
|
|
.forEach { group in
|
|
_ = try? LibSession.createGroupState(
|
|
groupSessionId: SessionId(.group, hex: group.id),
|
|
userED25519KeyPair: userEd25519KeyPair,
|
|
groupIdentityPrivateKey: group.groupIdentityPrivateKey,
|
|
shouldLoadState: true,
|
|
using: dependencies
|
|
)
|
|
}
|
|
}
|
|
|
|
private func loadState(
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
userEd25519SecretKey: [UInt8],
|
|
groupEd25519SecretKey: [UInt8]?,
|
|
cachedData: Data?
|
|
) throws -> Config {
|
|
// Setup initial variables (including getting the memory address for any cached data)
|
|
var conf: UnsafeMutablePointer<config_object>? = nil
|
|
var keysConf: UnsafeMutablePointer<config_group_keys>? = nil
|
|
var secretKey: [UInt8] = userEd25519SecretKey
|
|
var error: [CChar] = [CChar](repeating: 0, count: 256)
|
|
let cachedDump: (data: UnsafePointer<UInt8>, length: Int)? = cachedData?.withUnsafeBytes { unsafeBytes in
|
|
return unsafeBytes.baseAddress.map {
|
|
(
|
|
$0.assumingMemoryBound(to: UInt8.self),
|
|
unsafeBytes.count
|
|
)
|
|
}
|
|
}
|
|
let userConfigInitCalls: [ConfigDump.Variant: UserConfigInitialiser] = [
|
|
.userProfile: user_profile_init,
|
|
.contacts: contacts_init,
|
|
.convoInfoVolatile: convo_info_volatile_init,
|
|
.userGroups: user_groups_init
|
|
]
|
|
let groupConfigInitCalls: [ConfigDump.Variant: GroupConfigInitialiser] = [
|
|
.groupInfo: groups_info_init,
|
|
.groupMembers: groups_members_init
|
|
]
|
|
|
|
switch (variant, groupEd25519SecretKey) {
|
|
case (.invalid, _):
|
|
throw LibSessionError.unableToCreateConfigObject
|
|
.logging("Unable to create \(variant.rawValue) config object")
|
|
|
|
case (.userProfile, _), (.contacts, _), (.convoInfoVolatile, _), (.userGroups, _):
|
|
return try (userConfigInitCalls[variant]?(
|
|
&conf,
|
|
&secretKey,
|
|
cachedDump?.data,
|
|
(cachedDump?.length ?? 0),
|
|
&error
|
|
))
|
|
.toConfig(conf, variant: variant, error: error)
|
|
|
|
case (.groupInfo, .some(var adminSecretKey)), (.groupMembers, .some(var adminSecretKey)):
|
|
var identityPublicKey: [UInt8] = sessionId.publicKey
|
|
|
|
return try (groupConfigInitCalls[variant]?(
|
|
&conf,
|
|
&identityPublicKey,
|
|
&adminSecretKey,
|
|
cachedDump?.data,
|
|
(cachedDump?.length ?? 0),
|
|
&error
|
|
))
|
|
.toConfig(conf, variant: variant, error: error)
|
|
|
|
case (.groupKeys, .some(var adminSecretKey)):
|
|
var identityPublicKey: [UInt8] = sessionId.publicKey
|
|
|
|
guard
|
|
case .object(let infoConf) = configStore[sessionId, .groupInfo],
|
|
case .object(let membersConf) = configStore[sessionId, .groupMembers]
|
|
else {
|
|
throw LibSessionError.unableToCreateConfigObject
|
|
.logging("Unable to create \(variant.rawValue) config object: Group info and member config states not loaded")
|
|
}
|
|
|
|
return try groups_keys_init(
|
|
&keysConf,
|
|
&secretKey,
|
|
&identityPublicKey,
|
|
&adminSecretKey,
|
|
infoConf,
|
|
membersConf,
|
|
cachedDump?.data,
|
|
(cachedDump?.length ?? 0),
|
|
&error
|
|
)
|
|
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error)
|
|
|
|
// It looks like C doesn't deal will passing pointers to null variables well so we need
|
|
// to explicitly pass 'nil' for the admin key in this case
|
|
case (.groupInfo, .none), (.groupMembers, .none):
|
|
var identityPublicKey: [UInt8] = sessionId.publicKey
|
|
|
|
return try (groupConfigInitCalls[variant]?(
|
|
&conf,
|
|
&identityPublicKey,
|
|
nil,
|
|
cachedDump?.data,
|
|
(cachedDump?.length ?? 0),
|
|
&error
|
|
))
|
|
.toConfig(conf, variant: variant, error: error)
|
|
|
|
// It looks like C doesn't deal will passing pointers to null variables well so we need
|
|
// to explicitly pass 'nil' for the admin key in this case
|
|
case (.groupKeys, .none):
|
|
var identityPublicKey: [UInt8] = sessionId.publicKey
|
|
|
|
guard
|
|
case .object(let infoConf) = configStore[sessionId, .groupInfo],
|
|
case .object(let membersConf) = configStore[sessionId, .groupMembers]
|
|
else {
|
|
throw LibSessionError.unableToCreateConfigObject
|
|
.logging("Unable to create \(variant.rawValue) config object: Group info and member config states not loaded")
|
|
}
|
|
|
|
return try groups_keys_init(
|
|
&keysConf,
|
|
&secretKey,
|
|
&identityPublicKey,
|
|
nil,
|
|
infoConf,
|
|
membersConf,
|
|
cachedDump?.data,
|
|
(cachedDump?.length ?? 0),
|
|
&error
|
|
)
|
|
.toConfig(keysConf, info: infoConf, members: membersConf, variant: variant, error: error)
|
|
}
|
|
}
|
|
|
|
public func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> Config? {
|
|
return configStore[sessionId, variant]
|
|
}
|
|
|
|
public func setConfig(for variant: ConfigDump.Variant, sessionId: SessionId, to config: Config) {
|
|
configStore[sessionId, variant] = config
|
|
}
|
|
|
|
public func removeConfigs(for sessionId: SessionId) {
|
|
// First retrieve the configs stored for the sessionId
|
|
let configs: [LibSession.Config] = configStore[sessionId]
|
|
let keysConfig: LibSession.Config? = configs.first { config in
|
|
switch config {
|
|
case .groupKeys: return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// Then remove them from the ConfigStore (can't have something else accessing them)
|
|
ConfigDump.Variant.allCases.forEach { configStore[sessionId, $0] = nil }
|
|
|
|
// Finally we need to free them (if we got a `groupKeys` config then that includes
|
|
// the other confs for that sessionId so we can free them all at once, otherwise loop
|
|
// and freee everything
|
|
switch keysConfig {
|
|
case .groupKeys(let keysConf, let infoConf, let membersConf):
|
|
groups_keys_free(keysConf)
|
|
config_free(infoConf)
|
|
config_free(membersConf)
|
|
|
|
default:
|
|
configs.forEach { config in
|
|
switch config {
|
|
case .invalid, .groupKeys: break // Should be handled above
|
|
case .object(let conf): config_free(conf)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public func createDump(
|
|
config: Config?,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
timestampMs: Int64
|
|
) throws -> ConfigDump? {
|
|
// If it doesn't need a dump then do nothing
|
|
guard
|
|
configNeedsDump(config),
|
|
let dumpData: Data = try config?.dump()
|
|
else { return nil }
|
|
|
|
return ConfigDump(
|
|
variant: variant,
|
|
sessionId: sessionId.hexString,
|
|
data: dumpData,
|
|
timestampMs: timestampMs
|
|
)
|
|
}
|
|
|
|
// MARK: - Pushes
|
|
|
|
public func performAndPushChange(
|
|
_ db: Database,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
change: (Config?) throws -> ()
|
|
) throws {
|
|
guard let config: Config = configStore[sessionId, variant] else { return }
|
|
|
|
do {
|
|
// Peform the change
|
|
try change(config)
|
|
|
|
// If an error occurred during the change then actually throw it to prevent
|
|
// any database change from completing
|
|
try LibSessionError.throwIfNeeded(config)
|
|
|
|
// Only create a config dump if we need to
|
|
if configNeedsDump(config) {
|
|
try createDump(
|
|
config: config,
|
|
for: variant,
|
|
sessionId: sessionId,
|
|
timestampMs: dependencies[cache: .snodeAPI].currentOffsetTimestampMs()
|
|
)?.upsert(db)
|
|
}
|
|
}
|
|
catch {
|
|
Log.error(.libSession, "Failed to update/dump updated \(variant) config data due to error: \(error)")
|
|
throw error
|
|
}
|
|
|
|
// Make sure we need a push before scheduling one
|
|
guard config.needsPush else { return }
|
|
|
|
db.afterNextTransactionNestedOnce(dedupeId: LibSession.syncDedupeId(sessionId.hexString), using: dependencies) { [dependencies] db in
|
|
ConfigurationSyncJob.enqueue(db, swarmPublicKey: sessionId.hexString, using: dependencies)
|
|
}
|
|
}
|
|
|
|
public func pendingChanges(
|
|
_ db: Database,
|
|
swarmPubkey: String
|
|
) throws -> PendingChanges {
|
|
guard Identity.userExists(db, using: dependencies) else { throw LibSessionError.userDoesNotExist }
|
|
|
|
// Get a list of the different config variants for the provided publicKey
|
|
let userSessionId: SessionId = dependencies[cache: .general].sessionId
|
|
let targetVariants: [(sessionId: SessionId, variant: ConfigDump.Variant)] = {
|
|
switch (swarmPubkey, try? SessionId(from: swarmPubkey)) {
|
|
case (userSessionId.hexString, _):
|
|
return ConfigDump.Variant.userVariants.map { (userSessionId, $0) }
|
|
|
|
case (_, .some(let sessionId)) where sessionId.prefix == .group:
|
|
return ConfigDump.Variant.groupVariants.map { (sessionId, $0) }
|
|
|
|
default: return []
|
|
}
|
|
}()
|
|
|
|
// Extract any pending changes from the cached config entry for each variant
|
|
return try targetVariants
|
|
.sorted { (lhs: (SessionId, ConfigDump.Variant), rhs: (SessionId, ConfigDump.Variant)) in
|
|
lhs.1.sendOrder < rhs.1.sendOrder
|
|
}
|
|
.reduce(into: PendingChanges()) { result, info in
|
|
guard let config: Config = configStore[info.sessionId, info.variant] else { return }
|
|
|
|
// Add any obsolete hashes to be removed (want to do this even if there isn't a pending push
|
|
// to ensure we clean things up)
|
|
result.append(hashes: config.obsoleteHashes())
|
|
|
|
// Only generate the push data if we need to do a push
|
|
guard config.needsPush else { return }
|
|
|
|
guard let data: PendingChanges.PushData = config.push(variant: info.variant) else {
|
|
let configCountInfo: String = config.count(for: info.variant)
|
|
|
|
throw LibSessionError(
|
|
config,
|
|
fallbackError: .unableToGeneratePushData,
|
|
logMessage: "Failed to generate push data for \(info.variant) config data, size: \(configCountInfo), error"
|
|
)
|
|
}
|
|
|
|
result.append(data: data)
|
|
}
|
|
}
|
|
|
|
public func markingAsPushed(
|
|
seqNo: Int64,
|
|
serverHash: String,
|
|
sentTimestamp: Int64,
|
|
variant: ConfigDump.Variant,
|
|
swarmPublicKey: String
|
|
) -> ConfigDump? {
|
|
let sessionId: SessionId = SessionId(hex: swarmPublicKey, dumpVariant: variant)
|
|
|
|
guard let config: Config = configStore[sessionId, variant] else { return nil }
|
|
|
|
// Mark the config as pushed
|
|
config.confirmPushed(seqNo: seqNo, hash: serverHash)
|
|
|
|
// Update the result to indicate whether the config needs to be dumped
|
|
guard config.needsPush else { return nil }
|
|
|
|
return try? createDump(
|
|
config: config,
|
|
for: variant,
|
|
sessionId: sessionId,
|
|
timestampMs: sentTimestamp
|
|
)
|
|
}
|
|
|
|
// MARK: - Config Message Handling
|
|
|
|
public func configNeedsDump(_ config: LibSession.Config?) -> Bool {
|
|
switch config {
|
|
case .invalid, .none: return false
|
|
case .object(let conf): return config_needs_dump(conf)
|
|
case .groupKeys(let conf, _, _): return groups_keys_needs_dump(conf)
|
|
}
|
|
}
|
|
|
|
public func configHashes(for swarmPublicKey: String) -> [String] {
|
|
guard let sessionId: SessionId = try? SessionId(from: swarmPublicKey) else { return [] }
|
|
|
|
/// We `mutate` because `libSession` isn't thread safe and we don't want to worry about another thread messing
|
|
/// with the hashes while we retrieve them
|
|
return configStore[sessionId]
|
|
.compactMap { config in config.currentHashes() }
|
|
.reduce([], +)
|
|
}
|
|
|
|
public func handleConfigMessages(
|
|
_ db: Database,
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws {
|
|
guard !messages.isEmpty else { return }
|
|
guard !swarmPublicKey.isEmpty else { throw MessageReceiverError.noThread }
|
|
|
|
let groupedMessages: [ConfigDump.Variant: [ConfigMessageReceiveJob.Details.MessageInfo]] = messages
|
|
.grouped(by: { ConfigDump.Variant(namespace: $0.namespace) })
|
|
|
|
try groupedMessages
|
|
.sorted { lhs, rhs in lhs.key.namespace.processingOrder < rhs.key.namespace.processingOrder }
|
|
.forEach { variant, message in
|
|
let sessionId: SessionId = SessionId(hex: swarmPublicKey, dumpVariant: variant)
|
|
let config: Config? = configStore[sessionId, variant]
|
|
|
|
do {
|
|
// Merge the messages (if it doesn't merge anything then don't bother trying
|
|
// to handle the result)
|
|
guard let latestServerTimestampMs: Int64 = try config?.merge(message) else { return }
|
|
|
|
// Apply the updated states to the database
|
|
switch variant {
|
|
case .userProfile:
|
|
try handleUserProfileUpdate(
|
|
db,
|
|
in: config,
|
|
serverTimestampMs: latestServerTimestampMs
|
|
)
|
|
|
|
case .contacts:
|
|
try handleContactsUpdate(
|
|
db,
|
|
in: config,
|
|
serverTimestampMs: latestServerTimestampMs
|
|
)
|
|
|
|
case .convoInfoVolatile:
|
|
try handleConvoInfoVolatileUpdate(
|
|
db,
|
|
in: config
|
|
)
|
|
|
|
case .userGroups:
|
|
try handleUserGroupsUpdate(
|
|
db,
|
|
in: config,
|
|
serverTimestampMs: latestServerTimestampMs
|
|
)
|
|
|
|
case .groupInfo:
|
|
try handleGroupInfoUpdate(
|
|
db,
|
|
in: config,
|
|
groupSessionId: sessionId,
|
|
serverTimestampMs: latestServerTimestampMs
|
|
)
|
|
|
|
case .groupMembers:
|
|
try handleGroupMembersUpdate(
|
|
db,
|
|
in: config,
|
|
groupSessionId: sessionId,
|
|
serverTimestampMs: latestServerTimestampMs
|
|
)
|
|
|
|
case .groupKeys:
|
|
try handleGroupKeysUpdate(
|
|
db,
|
|
in: config,
|
|
groupSessionId: sessionId
|
|
)
|
|
|
|
case .invalid: Log.error(.libSession, "Failed to process merge of invalid config namespace")
|
|
}
|
|
|
|
// Need to check if the config needs to be dumped (this might have changed
|
|
// after handling the merge changes)
|
|
guard configNeedsDump(config) else {
|
|
try ConfigDump
|
|
.filter(
|
|
ConfigDump.Columns.variant == variant &&
|
|
ConfigDump.Columns.publicKey == sessionId.hexString
|
|
)
|
|
.updateAll(
|
|
db,
|
|
ConfigDump.Columns.timestampMs.set(to: latestServerTimestampMs)
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
try createDump(
|
|
config: config,
|
|
for: variant,
|
|
sessionId: sessionId,
|
|
timestampMs: latestServerTimestampMs
|
|
)?.upsert(db)
|
|
}
|
|
catch {
|
|
Log.error(.libSession, "Failed to process merge of \(variant) config data")
|
|
throw error
|
|
}
|
|
}
|
|
|
|
// Now that the local state has been updated, schedule a config sync if needed (this will
|
|
// push any pending updates and properly update the state)
|
|
db.afterNextTransactionNestedOnce(dedupeId: LibSession.syncDedupeId(swarmPublicKey), using: dependencies) { [dependencies] db in
|
|
ConfigurationSyncJob.enqueue(db, swarmPublicKey: swarmPublicKey, using: dependencies)
|
|
}
|
|
}
|
|
|
|
public func unsafeDirectMergeConfigMessage(
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws {
|
|
guard !messages.isEmpty else { return }
|
|
|
|
let groupedMessages: [ConfigDump.Variant: [ConfigMessageReceiveJob.Details.MessageInfo]] = messages
|
|
.grouped(by: { ConfigDump.Variant(namespace: $0.namespace) })
|
|
|
|
try groupedMessages
|
|
.sorted { lhs, rhs in lhs.key.namespace.processingOrder < rhs.key.namespace.processingOrder }
|
|
.forEach { [configStore] variant, message in
|
|
let sessionId: SessionId = SessionId(hex: swarmPublicKey, dumpVariant: variant)
|
|
_ = try configStore[sessionId, variant]?.merge(message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - SessionUtilCacheType
|
|
|
|
/// This is a read-only version of the Cache designed to avoid unintentionally mutating the instance in a non-thread-safe way
|
|
public protocol LibSessionImmutableCacheType: ImmutableCacheType {
|
|
var userSessionId: SessionId { get }
|
|
var isEmpty: Bool { get }
|
|
}
|
|
|
|
/// The majority `libSession` functions can only be accessed via the mutable cache because `libSession` isn't thread safe so if we try
|
|
/// to read/write values while another thread is touching the same data then the app can crash due to bad memory issues
|
|
public protocol LibSessionCacheType: LibSessionImmutableCacheType, MutableCacheType {
|
|
var dependencies: Dependencies { get }
|
|
var userSessionId: SessionId { get }
|
|
var isEmpty: Bool { get }
|
|
var needsSync: Bool { get }
|
|
|
|
// MARK: - State Management
|
|
|
|
func loadState(_ db: Database)
|
|
func loadDefaultStatesFor(
|
|
userConfigVariants: Set<ConfigDump.Variant>,
|
|
groups: [ClosedGroup],
|
|
userSessionId: SessionId,
|
|
userEd25519KeyPair: KeyPair
|
|
)
|
|
func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config?
|
|
func setConfig(for variant: ConfigDump.Variant, sessionId: SessionId, to config: LibSession.Config)
|
|
func removeConfigs(for sessionId: SessionId)
|
|
func createDump(
|
|
config: LibSession.Config?,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
timestampMs: Int64
|
|
) throws -> ConfigDump?
|
|
|
|
// MARK: - Pushes
|
|
|
|
func performAndPushChange(
|
|
_ db: Database,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
change: @escaping (LibSession.Config?) throws -> ()
|
|
) throws
|
|
func pendingChanges(_ db: Database, swarmPubkey: String) throws -> LibSession.PendingChanges
|
|
func markingAsPushed(
|
|
seqNo: Int64,
|
|
serverHash: String,
|
|
sentTimestamp: Int64,
|
|
variant: ConfigDump.Variant,
|
|
swarmPublicKey: String
|
|
) -> ConfigDump?
|
|
|
|
// MARK: - Config Message Handling
|
|
|
|
func configNeedsDump(_ config: LibSession.Config?) -> Bool
|
|
func configHashes(for swarmPubkey: String) -> [String]
|
|
|
|
func handleConfigMessages(
|
|
_ db: Database,
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws
|
|
|
|
/// This function takes config messages and just triggers the merge into `libSession`
|
|
///
|
|
/// **Note:** This function should only be used in a situation where we want to retrieve the data from a config message as using it
|
|
/// elsewhere will result in the database getting out of sync with the config state
|
|
func unsafeDirectMergeConfigMessage(
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws
|
|
}
|
|
|
|
private final class NoopLibSessionCache: LibSessionCacheType {
|
|
let dependencies: Dependencies
|
|
let userSessionId: SessionId = .invalid
|
|
let isEmpty: Bool = true
|
|
let needsSync: Bool = false
|
|
|
|
init(using dependencies: Dependencies) {
|
|
self.dependencies = dependencies
|
|
}
|
|
|
|
// MARK: - State Management
|
|
|
|
func loadState(_ db: Database) {}
|
|
func loadDefaultStatesFor(
|
|
userConfigVariants: Set<ConfigDump.Variant>,
|
|
groups: [ClosedGroup],
|
|
userSessionId: SessionId,
|
|
userEd25519KeyPair: KeyPair
|
|
) {}
|
|
func config(for variant: ConfigDump.Variant, sessionId: SessionId) -> LibSession.Config? { return nil }
|
|
func setConfig(for variant: ConfigDump.Variant, sessionId: SessionId, to config: LibSession.Config) {}
|
|
func removeConfigs(for sessionId: SessionId) {}
|
|
func createDump(
|
|
config: LibSession.Config?,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
timestampMs: Int64
|
|
) throws -> ConfigDump? {
|
|
return nil
|
|
}
|
|
|
|
// MARK: - Pushes
|
|
|
|
func performAndPushChange(
|
|
_ db: Database,
|
|
for variant: ConfigDump.Variant,
|
|
sessionId: SessionId,
|
|
change: (LibSession.Config?) throws -> ()
|
|
) throws {}
|
|
|
|
func pendingChanges(_ db: GRDB.Database, swarmPubkey: String) throws -> LibSession.PendingChanges {
|
|
return LibSession.PendingChanges()
|
|
}
|
|
|
|
func markingAsPushed(seqNo: Int64, serverHash: String, sentTimestamp: Int64, variant: ConfigDump.Variant, swarmPublicKey: String) -> ConfigDump? {
|
|
return nil
|
|
}
|
|
|
|
// MARK: - Config Message Handling
|
|
|
|
func configNeedsDump(_ config: LibSession.Config?) -> Bool { return false }
|
|
func configHashes(for swarmPubkey: String) -> [String] { return [] }
|
|
func handleConfigMessages(
|
|
_ db: Database,
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws {}
|
|
func unsafeDirectMergeConfigMessage(
|
|
swarmPublicKey: String,
|
|
messages: [ConfigMessageReceiveJob.Details.MessageInfo]
|
|
) throws {}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private extension Optional where Wrapped == Int32 {
|
|
func toConfig(
|
|
_ maybeConf: UnsafeMutablePointer<config_object>?,
|
|
variant: ConfigDump.Variant,
|
|
error: [CChar]
|
|
) throws -> LibSession.Config {
|
|
guard self == 0, let conf: UnsafeMutablePointer<config_object> = maybeConf else {
|
|
throw LibSessionError.unableToCreateConfigObject
|
|
.logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))")
|
|
}
|
|
|
|
switch variant {
|
|
case .userProfile, .contacts, .convoInfoVolatile,
|
|
.userGroups, .groupInfo, .groupMembers:
|
|
return .object(conf)
|
|
|
|
case .groupKeys, .invalid: throw LibSessionError.unableToCreateConfigObject
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension Int32 {
|
|
func toConfig(
|
|
_ maybeConf: UnsafeMutablePointer<config_group_keys>?,
|
|
info: UnsafeMutablePointer<config_object>,
|
|
members: UnsafeMutablePointer<config_object>,
|
|
variant: ConfigDump.Variant,
|
|
error: [CChar]
|
|
) throws -> LibSession.Config {
|
|
guard self == 0, let conf: UnsafeMutablePointer<config_group_keys> = maybeConf else {
|
|
throw LibSessionError.unableToCreateConfigObject
|
|
.logging("Unable to create \(variant.rawValue) config object: \(String(cString: error))")
|
|
}
|
|
|
|
switch variant {
|
|
case .groupKeys: return .groupKeys(conf, info: info, members: members)
|
|
default: throw LibSessionError.unableToCreateConfigObject
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension SessionId {
|
|
init(hex: String, dumpVariant: ConfigDump.Variant) {
|
|
switch (try? SessionId(from: hex), dumpVariant) {
|
|
case (.some(let sessionId), _): self = sessionId
|
|
case (_, .userProfile), (_, .contacts), (_, .convoInfoVolatile), (_, .userGroups):
|
|
self = SessionId(.standard, hex: hex)
|
|
|
|
case (_, .groupInfo), (_, .groupMembers), (_, .groupKeys):
|
|
self = SessionId(.group, hex: hex)
|
|
|
|
case (_, .invalid): self = SessionId.invalid
|
|
}
|
|
}
|
|
}
|