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/Config Handling/LibSession+Contacts.swift

831 lines
37 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtil
import SessionUtilitiesKit
// MARK: - Size Restrictions
public extension LibSession {
static var sizeMaxNameBytes: Int { CONTACT_MAX_NAME_LENGTH }
static var sizeMaxNicknameBytes: Int { CONTACT_MAX_NAME_LENGTH }
static var sizeMaxProfileUrlBytes: Int { PROFILE_PIC_MAX_URL_LENGTH }
}
// MARK: - Contacts Handling
internal extension LibSession {
static let columnsRelatedToContacts: [ColumnExpression] = [
Contact.Columns.isApproved,
Contact.Columns.isBlocked,
Contact.Columns.didApproveMe,
Profile.Columns.name,
Profile.Columns.nickname,
Profile.Columns.profilePictureUrl,
Profile.Columns.profileEncryptionKey,
DisappearingMessagesConfiguration.Columns.isEnabled,
DisappearingMessagesConfiguration.Columns.type,
DisappearingMessagesConfiguration.Columns.durationSeconds
]
}
// MARK: - Incoming Changes
internal extension LibSessionCacheType {
func handleContactsUpdate(
_ db: Database,
in config: LibSession.Config?,
serverTimestampMs: Int64
) throws {
guard configNeedsDump(config) else { return }
guard case .contacts(let conf) = config else { throw LibSessionError.invalidConfigObject }
// The current users contact data is handled separately so exclude it if it's present (as that's
// actually a bug)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let targetContactData: [String: ContactData] = try LibSession.extractContacts(
from: conf,
serverTimestampMs: serverTimestampMs,
using: dependencies
).filter { $0.key != userSessionId.hexString }
// Since we don't sync 100% of the data stored against the contact and profile objects we
// need to only update the data we do have to ensure we don't overwrite anything that doesn't
// get synced
try targetContactData
.forEach { sessionId, data in
// Note: We only update the contact and profile records if the data has actually changed
// in order to avoid triggering UI updates for every thread on the home screen (the DB
// observation system can't differ between update calls which do and don't change anything)
let contact: Contact = Contact.fetchOrCreate(db, id: sessionId, using: dependencies)
let profile: Profile = Profile.fetchOrCreate(db, id: sessionId)
let profileNameShouldBeUpdated: Bool = (
!data.profile.name.isEmpty &&
profile.name != data.profile.name &&
(profile.lastNameUpdate ?? 0) < (data.profile.lastNameUpdate ?? 0)
)
let profilePictureShouldBeUpdated: Bool = (
(
profile.profilePictureUrl != data.profile.profilePictureUrl ||
profile.profileEncryptionKey != data.profile.profileEncryptionKey
) &&
(profile.lastProfilePictureUpdate ?? 0) < (data.profile.lastProfilePictureUpdate ?? 0)
)
if
profileNameShouldBeUpdated ||
profile.nickname != data.profile.nickname ||
profilePictureShouldBeUpdated
{
try profile.upsert(db)
try Profile
.filter(id: sessionId)
.updateAllAndConfig(
db,
[
(!profileNameShouldBeUpdated ? nil :
Profile.Columns.name.set(to: data.profile.name)
),
(!profileNameShouldBeUpdated ? nil :
Profile.Columns.lastNameUpdate.set(to: data.profile.lastNameUpdate)
),
(profile.nickname == data.profile.nickname ? nil :
Profile.Columns.nickname.set(to: data.profile.nickname)
),
(profile.profilePictureUrl != data.profile.profilePictureUrl ? nil :
Profile.Columns.profilePictureUrl.set(to: data.profile.profilePictureUrl)
),
(profile.profileEncryptionKey != data.profile.profileEncryptionKey ? nil :
Profile.Columns.profileEncryptionKey.set(to: data.profile.profileEncryptionKey)
),
(!profilePictureShouldBeUpdated ? nil :
Profile.Columns.lastProfilePictureUpdate.set(to: data.profile.lastProfilePictureUpdate)
)
].compactMap { $0 },
using: dependencies
)
}
/// Since message requests have no reverse, we should only handle setting `isApproved`
/// and `didApproveMe` to `true`. This may prevent some weird edge cases where a config message
/// swapping `isApproved` and `didApproveMe` to `false`
if
(contact.isApproved != data.contact.isApproved) ||
(contact.isBlocked != data.contact.isBlocked) ||
(contact.didApproveMe != data.contact.didApproveMe)
{
try contact.upsert(db)
try Contact
.filter(id: sessionId)
.updateAllAndConfig(
db,
[
(!data.contact.isApproved || contact.isApproved == data.contact.isApproved ? nil :
Contact.Columns.isApproved.set(to: true)
),
(contact.isBlocked == data.contact.isBlocked ? nil :
Contact.Columns.isBlocked.set(to: data.contact.isBlocked)
),
(!data.contact.didApproveMe || contact.didApproveMe == data.contact.didApproveMe ? nil :
Contact.Columns.didApproveMe.set(to: true)
)
].compactMap { $0 },
using: dependencies
)
}
/// If the contact's `hidden` flag doesn't match the visibility of their conversation then create/delete the
/// associated contact conversation accordingly
let threadInfo: LibSession.PriorityVisibilityInfo? = try? SessionThread
.filter(id: sessionId)
.select(.id, .variant, .pinnedPriority, .shouldBeVisible)
.asRequest(of: LibSession.PriorityVisibilityInfo.self)
.fetchOne(db)
let threadExists: Bool = (threadInfo != nil)
let updatedShouldBeVisible: Bool = LibSession.shouldBeVisible(priority: data.priority)
switch (updatedShouldBeVisible, threadExists) {
/// If we are hiding the conversation then kick the user from it if it's currently open then delete the thread
case (false, true):
LibSession.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId], using: dependencies)
try SessionThread.deleteOrLeave(
db,
type: .deleteContactConversationAndMarkHidden,
threadId: sessionId,
threadVariant: .contact,
using: dependencies
)
/// We need to create or update the thread
case (true, _):
let localConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration
.fetchOne(db, id: sessionId)
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(sessionId))
let disappearingMessagesConfigChanged: Bool = (
data.config.isValidV2Config() &&
data.config != localConfig
)
_ = try SessionThread.upsert(
db,
id: sessionId,
variant: .contact,
values: SessionThread.TargetValues(
creationDateTimestamp: .setTo(data.created),
shouldBeVisible: .setTo(updatedShouldBeVisible),
pinnedPriority: .setTo(data.priority),
disappearingMessagesConfig: (disappearingMessagesConfigChanged ?
.setTo(data.config) :
.useExisting
)
),
using: dependencies
)
/// Thread shouldn't be visible and doesn't exist so no need to do anything
case (false, false): break
}
}
/// Delete any contact/thread records which aren't in the config message
let syncedContactIds: [String] = targetContactData
.map { $0.key }
.appending(userSessionId.hexString)
let contactIdsToRemove: [String] = try Contact
.filter(!syncedContactIds.contains(Contact.Columns.id))
.select(.id)
.asRequest(of: String.self)
.fetchAll(db)
let threadIdsToRemove: [String] = try SessionThread
.filter(!syncedContactIds.contains(SessionThread.Columns.id))
.filter(SessionThread.Columns.variant == SessionThread.Variant.contact)
.filter(
/// Only want to include include standard contact conversations (not blinded conversations)
SessionThread.Columns.id > SessionId.Prefix.standard.rawValue &&
SessionThread.Columns.id < SessionId.Prefix.standard.endOfRangeString
)
.select(.id)
.asRequest(of: String.self)
.fetchAll(db)
/// When the user opens a brand new conversation this creates a "draft conversation" which has a hidden thread but no
/// contact record, when we receive a contact update this "draft conversation" would be included in the
/// `threadIdsToRemove` which would result in the user getting kicked from the screen and the thread removed, we
/// want to avoid this (as it's essentially a bug) so find any conversations in this state and remove them from the list that
/// will be pruned
let threadT: TypedTableAlias<SessionThread> = TypedTableAlias()
let contactT: TypedTableAlias<Contact> = TypedTableAlias()
let draftConversationIds: [String] = try SQLRequest<String>("""
SELECT \(threadT[.id])
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contactT[.id]) = \(threadT[.id])
WHERE (
\(SQL("\(threadT[.id]) IN \(threadIdsToRemove)")) AND
\(contactT[.id]) IS NULL
)
""").fetchAll(db)
/// Consolidate the ids which should be removed
let combinedIds: [String] = contactIdsToRemove
.appending(contentsOf: threadIdsToRemove)
.filter { !draftConversationIds.contains($0) }
if !combinedIds.isEmpty {
LibSession.kickFromConversationUIIfNeeded(removedThreadIds: combinedIds, using: dependencies)
try Contact
.filter(ids: combinedIds)
.deleteAll(db)
// Also need to remove any 'nickname' values since they are associated to contact data
try Profile
.filter(ids: combinedIds)
.updateAllAndConfig(
db,
Profile.Columns.nickname.set(to: nil),
using: dependencies
)
// Delete the one-to-one conversations associated to the contact
try SessionThread.deleteOrLeave(
db,
type: .deleteContactConversationAndContact,
threadIds: combinedIds,
threadVariant: .contact,
using: dependencies
)
try LibSession.remove(
db,
volatileContactIds: combinedIds,
using: dependencies
)
}
}
}
// MARK: - Outgoing Changes
internal extension LibSession {
static func upsert(
contactData: [SyncedContactInfo],
in config: Config?,
using dependencies: Dependencies
) throws {
guard case .contacts(let conf) = config else { throw LibSessionError.invalidConfigObject }
// The current users contact data doesn't need to sync so exclude it, we also don't want to sync
// blinded message requests so exclude those as well
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let targetContacts: [SyncedContactInfo] = contactData
.filter {
$0.id != userSessionId.hexString &&
(try? SessionId(from: $0.id))?.prefix == .standard
}
// If we only updated the current user contact then no need to continue
guard !targetContacts.isEmpty else { return }
// Update the name
try targetContacts
.forEach { info in
var contact: contacts_contact = contacts_contact()
guard
var sessionId: [CChar] = info.id.cString(using: .utf8),
contacts_get_or_construct(conf, &contact, &sessionId)
else {
/// It looks like there are some situations where this object might not get created correctly (and
/// will throw due to the implicit unwrapping) as a result we put it in a guard and throw instead
throw LibSessionError(
conf,
fallbackError: .getOrConstructFailedUnexpectedly,
logMessage: "Unable to upsert contact to LibSession"
)
}
// Assign all properties to match the updated contact (if there is one)
if
let isApproved: Bool = info.isApproved,
let didApproveMe: Bool = info.didApproveMe,
let isBlocked: Bool = info.isBlocked
{
contact.approved = isApproved
contact.approved_me = didApproveMe
contact.blocked = isBlocked
// If we were given a `created` timestamp then set it to the min between the current
// setting and the value (as long as the current setting isn't `0`)
if let created: Int64 = info.created.map({ Int64(floor($0)) }) {
contact.created = (contact.created > 0 ? min(contact.created, created) : created)
}
// Store the updated contact (needs to happen before variables go out of scope)
contacts_set(conf, &contact)
try LibSessionError.throwIfNeeded(conf)
}
// Update the profile data (if there is one - users we have sent a message request to may
// not have profile info in certain situations)
if let updatedName: String = info.name {
let oldAvatarUrl: String? = contact.get(\.profile_pic.url)
let oldAvatarKey: Data? = contact.get(\.profile_pic.key)
contact.set(\.name, to: updatedName)
contact.set(\.nickname, to: info.nickname)
contact.set(\.profile_pic.url, to: info.profilePictureUrl)
contact.set(\.profile_pic.key, to: info.profileEncryptionKey)
// Attempts retrieval of the profile picture (will schedule a download if
// needed via a throttled subscription on another thread to prevent blocking)
//
// Note: Only trigger the avatar download if we are in the main app (don't
// want the extensions to trigger this as it can clog up their networking)
if
let updatedProfile: Profile = info.profile,
dependencies[singleton: .appContext].isMainApp && (
oldAvatarUrl != (info.profilePictureUrl ?? "") ||
oldAvatarKey != (info.profileEncryptionKey ?? Data(repeating: 0, count: DisplayPictureManager.aes256KeyByteLength))
)
{
dependencies[singleton: .displayPictureManager].displayPicture(
owner: .user(updatedProfile)
)
}
// Store the updated contact (needs to happen before variables go out of scope)
contacts_set(conf, &contact)
try LibSessionError.throwIfNeeded(conf)
}
// Assign all properties to match the updated disappearing messages configuration (if there is one)
if
let disappearingInfo: LibSession.DisappearingMessageInfo = info.disappearingMessagesInfo,
let exp_mode: CONVO_EXPIRATION_MODE = disappearingInfo.type?.toLibSession()
{
contact.exp_mode = exp_mode
contact.exp_seconds = Int32(disappearingInfo.durationSeconds)
}
// Store the updated contact (can't be sure if we made any changes above)
contact.priority = (info.priority ?? contact.priority)
contacts_set(conf, &contact)
try LibSessionError.throwIfNeeded(conf)
}
}
}
// MARK: - Outgoing Changes
internal extension LibSession {
static func updatingContacts<T>(
_ db: Database,
_ updated: [T],
using dependencies: Dependencies
) throws -> [T] {
guard let updatedContacts: [Contact] = updated as? [Contact] else { throw StorageError.generic }
// The current users contact data doesn't need to sync so exclude it, we also don't want to sync
// blinded message requests so exclude those as well
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let targetContacts: [Contact] = updatedContacts
.filter {
$0.id != userSessionId.hexString &&
(try? SessionId(from: $0.id))?.prefix == .standard
}
// If we only updated the current user contact then no need to continue
guard !targetContacts.isEmpty else { return updated }
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in
guard case .contacts(let conf) = config else { throw LibSessionError.invalidConfigObject }
// When inserting new contacts (or contacts with invalid profile data) we want
// to add any valid profile information we have so identify if any of the updated
// contacts are new/invalid, and if so, fetch any profile data we have for them
let newContactIds: [String] = targetContacts
.compactMap { contactData -> String? in
var contact: contacts_contact = contacts_contact()
guard
var cContactId: [CChar] = contactData.id.cString(using: .utf8),
contacts_get(conf, &contact, &cContactId),
contact.get(\.name, nullIfEmpty: true) != nil
else {
LibSessionError.clear(conf)
return contactData.id
}
return nil
}
let newProfiles: [String: Profile] = try Profile
.fetchAll(db, ids: newContactIds)
.reduce(into: [:]) { result, next in result[next.id] = next }
// Upsert the updated contact data
try LibSession
.upsert(
contactData: targetContacts
.map { contact in
SyncedContactInfo(
id: contact.id,
contact: contact,
profile: newProfiles[contact.id]
)
},
in: config,
using: dependencies
)
}
}
return updated
}
static func updatingProfiles<T>(
_ db: Database,
_ updated: [T],
using dependencies: Dependencies
) throws -> [T] {
guard let updatedProfiles: [Profile] = updated as? [Profile] else { throw StorageError.generic }
// We should only sync profiles which are associated to contact data to avoid including profiles
// for random people in community conversations so filter out any profiles which don't have an
// associated contact
let existingContactIds: [String] = (try? Contact
.filter(ids: updatedProfiles.map { $0.id })
.select(.id)
.asRequest(of: String.self)
.fetchAll(db))
.defaulting(to: [])
// If none of the profiles are associated with existing contacts then ignore the changes (no need
// to do a config sync)
guard !existingContactIds.isEmpty else { return updated }
// Get the user public key (updating their profile is handled separately)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let targetProfiles: [Profile] = updatedProfiles
.filter {
$0.id != userSessionId.hexString &&
(try? SessionId(from: $0.id))?.prefix == .standard &&
existingContactIds.contains($0.id)
}
// Update the user profile first (if needed)
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userSessionId.hexString }) {
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .userProfile, sessionId: userSessionId) { config in
try LibSession.update(
profile: updatedUserProfile,
in: config
)
}
}
}
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in
try LibSession
.upsert(
contactData: targetProfiles
.map { SyncedContactInfo(id: $0.id, profile: $0) },
in: config,
using: dependencies
)
}
}
return updated
}
@discardableResult static func updatingDisappearingConfigsOneToOne<T>(
_ db: Database,
_ updated: [T],
using dependencies: Dependencies
) throws -> [T] {
guard let updatedDisappearingConfigs: [DisappearingMessagesConfiguration] = updated as? [DisappearingMessagesConfiguration] else { throw StorageError.generic }
// Filter out any disappearing config changes related to groups
let targetUpdatedConfigs: [DisappearingMessagesConfiguration] = updatedDisappearingConfigs
.filter { (try? SessionId.Prefix(from: $0.id)) != .group }
guard !targetUpdatedConfigs.isEmpty else { return updated }
// We should only sync disappearing messages configs which are associated to existing contacts
let existingContactIds: [String] = (try? Contact
.filter(ids: targetUpdatedConfigs.map { $0.id })
.select(.id)
.asRequest(of: String.self)
.fetchAll(db))
.defaulting(to: [])
// If none of the disappearing messages configs are associated with existing contacts then ignore
// the changes (no need to do a config sync)
guard !existingContactIds.isEmpty else { return updated }
// Get the user public key (updating note to self is handled separately)
let userSessionId: SessionId = dependencies[cache: .general].sessionId
let targetDisappearingConfigs: [DisappearingMessagesConfiguration] = targetUpdatedConfigs
.filter {
$0.id != userSessionId.hexString &&
(try? SessionId(from: $0.id))?.prefix == .standard &&
existingContactIds.contains($0.id)
}
// Update the note to self disappearing messages config first (if needed)
if let updatedUserDisappearingConfig: DisappearingMessagesConfiguration = targetUpdatedConfigs.first(where: { $0.id == userSessionId.hexString }) {
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .userProfile, sessionId: userSessionId) { config in
try LibSession.updateNoteToSelf(
disappearingMessagesConfig: updatedUserDisappearingConfig,
in: config
)
}
}
}
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in
try LibSession
.upsert(
contactData: targetDisappearingConfigs
.map { SyncedContactInfo(id: $0.id, disappearingMessagesConfig: $0) },
in: config,
using: dependencies
)
}
}
return updated
}
}
// MARK: - External Outgoing Changes
public extension LibSession {
static func hide(
_ db: Database,
contactIds: [String],
using dependencies: Dependencies
) throws {
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: dependencies[cache: .general].sessionId) { config in
// Mark the contacts as hidden
try LibSession.upsert(
contactData: contactIds
.map {
SyncedContactInfo(
id: $0,
priority: LibSession.hiddenPriority
)
},
in: config,
using: dependencies
)
}
}
}
static func remove(
_ db: Database,
contactIds: [String],
using dependencies: Dependencies
) throws {
guard !contactIds.isEmpty else { return }
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: dependencies[cache: .general].sessionId) { config in
guard case .contacts(let conf) = config else { throw LibSessionError.invalidConfigObject }
contactIds.forEach { sessionId in
guard var cSessionId: [CChar] = sessionId.cString(using: .utf8) else { return }
// Don't care if the contact doesn't exist
contacts_erase(conf, &cSessionId)
}
}
}
}
static func update(
_ db: Database,
sessionId: String,
disappearingMessagesConfig: DisappearingMessagesConfiguration,
using dependencies: Dependencies
) throws {
let userSessionId: SessionId = dependencies[cache: .general].sessionId
switch sessionId {
case userSessionId.hexString:
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .userProfile, sessionId: userSessionId) { config in
try LibSession.updateNoteToSelf(
disappearingMessagesConfig: disappearingMessagesConfig,
in: config
)
}
}
default:
try dependencies.mutate(cache: .libSession) { cache in
try cache.performAndPushChange(db, for: .contacts, sessionId: userSessionId) { config in
try LibSession
.upsert(
contactData: [
SyncedContactInfo(
id: sessionId,
disappearingMessagesConfig: disappearingMessagesConfig
)
],
in: config,
using: dependencies
)
}
}
}
}
}
// MARK: - SyncedContactInfo
extension LibSession {
struct SyncedContactInfo {
let id: String
let isTrusted: Bool?
let isApproved: Bool?
let isBlocked: Bool?
let didApproveMe: Bool?
let name: String?
let nickname: String?
let profilePictureUrl: String?
let profileEncryptionKey: Data?
let disappearingMessagesInfo: DisappearingMessageInfo?
let priority: Int32?
let created: TimeInterval?
fileprivate var profile: Profile? {
guard let name: String = name else { return nil }
return Profile(
id: id,
name: name,
nickname: nickname,
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: profileEncryptionKey
)
}
init(
id: String,
contact: Contact? = nil,
profile: Profile? = nil,
disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil,
priority: Int32? = nil,
created: TimeInterval? = nil
) {
self.init(
id: id,
isTrusted: contact?.isTrusted,
isApproved: contact?.isApproved,
isBlocked: contact?.isBlocked,
didApproveMe: contact?.didApproveMe,
name: profile?.name,
nickname: profile?.nickname,
profilePictureUrl: profile?.profilePictureUrl,
profileEncryptionKey: profile?.profileEncryptionKey,
disappearingMessagesInfo: disappearingMessagesConfig.map {
DisappearingMessageInfo(
isEnabled: $0.isEnabled,
durationSeconds: Int64($0.durationSeconds),
rawType: $0.type?.rawValue
)
},
priority: priority,
created: created
)
}
init(
id: String,
isTrusted: Bool? = nil,
isApproved: Bool? = nil,
isBlocked: Bool? = nil,
didApproveMe: Bool? = nil,
name: String? = nil,
nickname: String? = nil,
profilePictureUrl: String? = nil,
profileEncryptionKey: Data? = nil,
disappearingMessagesInfo: DisappearingMessageInfo? = nil,
priority: Int32? = nil,
created: TimeInterval? = nil
) {
self.id = id
self.isTrusted = isTrusted
self.isApproved = isApproved
self.isBlocked = isBlocked
self.didApproveMe = didApproveMe
self.name = name
self.nickname = nickname
self.profilePictureUrl = profilePictureUrl
self.profileEncryptionKey = profileEncryptionKey
self.disappearingMessagesInfo = disappearingMessagesInfo
self.priority = priority
self.created = created
}
}
struct DisappearingMessageInfo {
let isEnabled: Bool
let durationSeconds: Int64
let rawType: Int?
var type: DisappearingMessagesConfiguration.DisappearingMessageType? {
rawType.map { DisappearingMessagesConfiguration.DisappearingMessageType(rawValue: $0) }
}
func generateConfig(for threadId: String) -> DisappearingMessagesConfiguration {
DisappearingMessagesConfiguration(
threadId: threadId,
isEnabled: isEnabled,
durationSeconds: TimeInterval(durationSeconds),
type: rawType.map { DisappearingMessagesConfiguration.DisappearingMessageType(rawValue: $0) }
)
}
}
}
// MARK: - ContactData
private struct ContactData {
let contact: Contact
let profile: Profile
let config: DisappearingMessagesConfiguration
let priority: Int32
let created: TimeInterval
}
// MARK: - Convenience
private extension LibSession {
static func extractContacts(
from conf: UnsafeMutablePointer<config_object>?,
serverTimestampMs: Int64,
using dependencies: Dependencies
) throws -> [String: ContactData] {
var infiniteLoopGuard: Int = 0
var result: [String: ContactData] = [:]
var contact: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact) {
try LibSession.checkLoopLimitReached(&infiniteLoopGuard, for: .contacts)
let contactId: String = contact.get(\.session_id)
let contactResult: Contact = Contact(
id: contactId,
isApproved: contact.approved,
isBlocked: contact.blocked,
didApproveMe: contact.approved_me,
using: dependencies
)
let profilePictureUrl: String? = contact.get(\.profile_pic.url, nullIfEmpty: true)
let profileResult: Profile = Profile(
id: contactId,
name: contact.get(\.name),
lastNameUpdate: (TimeInterval(serverTimestampMs) / 1000),
nickname: contact.get(\.nickname, nullIfEmpty: true),
profilePictureUrl: profilePictureUrl,
profileEncryptionKey: (profilePictureUrl == nil ? nil : contact.get(\.profile_pic.key)),
lastProfilePictureUpdate: (TimeInterval(serverTimestampMs) / 1000)
)
let configResult: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration(
threadId: contactId,
isEnabled: contact.exp_seconds > 0,
durationSeconds: TimeInterval(contact.exp_seconds),
type: DisappearingMessagesConfiguration.DisappearingMessageType(libSessionType: contact.exp_mode)
)
result[contactId] = ContactData(
contact: contactResult,
profile: profileResult,
config: configResult,
priority: contact.priority,
created: TimeInterval(contact.created)
)
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
return result
}
}
// MARK: - C Conformance
extension contacts_contact: CAccessible & CMutable {}