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.
session-ios/SessionMessagingKit/LibSession/LibSession+SessionMessaging...

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