mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
297 lines
12 KiB
Swift
297 lines
12 KiB
Swift
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import SessionUtil
|
|
import SessionUtilitiesKit
|
|
|
|
// MARK: - LibSession
|
|
|
|
internal extension LibSession {
|
|
static let columnsRelatedToUserProfile: [Profile.Columns] = [
|
|
Profile.Columns.name,
|
|
Profile.Columns.displayPictureUrl,
|
|
Profile.Columns.displayPictureEncryptionKey,
|
|
Profile.Columns.profileLastUpdated
|
|
]
|
|
|
|
static let syncedSettings: [String] = [
|
|
Setting.BoolKey.checkForCommunityMessageRequests.rawValue
|
|
]
|
|
}
|
|
|
|
// MARK: - Incoming Changes
|
|
|
|
internal extension LibSessionCacheType {
|
|
func handleUserProfileUpdate(
|
|
_ db: ObservingDatabase,
|
|
in config: LibSession.Config?,
|
|
oldState: [ObservableKey: Any]
|
|
) throws {
|
|
guard configNeedsDump(config) else { return }
|
|
guard case .userProfile(let conf) = config else {
|
|
throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: config)
|
|
}
|
|
|
|
// A profile must have a name so if this is null then it's invalid and can be ignored
|
|
guard let profileNamePtr: UnsafePointer<CChar> = user_profile_get_name(conf) else { return }
|
|
|
|
let profileName: String = String(cString: profileNamePtr)
|
|
let displayPic: user_profile_pic = user_profile_get_pic(conf)
|
|
let displayPictureUrl: String? = displayPic.get(\.url, nullIfEmpty: true)
|
|
let displayPictureEncryptionKey: Data? = displayPic.get(\.key, nullIfEmpty: true)
|
|
let profileLastUpdateTimestamp: TimeInterval = TimeInterval(user_profile_get_profile_updated(conf))
|
|
|
|
// Handle user profile changes
|
|
try Profile.updateIfNeeded(
|
|
db,
|
|
publicKey: userSessionId.hexString,
|
|
displayNameUpdate: .currentUserUpdate(profileName),
|
|
displayPictureUpdate: {
|
|
guard
|
|
let displayPictureUrl: String = displayPictureUrl,
|
|
let displayPictureEncryptionKey: Data = displayPictureEncryptionKey
|
|
else { return .currentUserRemove }
|
|
|
|
return .currentUserUpdateTo(
|
|
url: displayPictureUrl,
|
|
key: displayPictureEncryptionKey,
|
|
sessionProProof: getCurrentUserProProof(), // TODO: double check if this is needed after Pro Proof is implemented
|
|
isReupload: false
|
|
)
|
|
}(),
|
|
profileUpdateTimestamp: profileLastUpdateTimestamp,
|
|
cacheSource: .value((oldState[.profile(userSessionId.hexString)] as? Profile), fallback: .database),
|
|
suppressUserProfileConfigUpdate: true,
|
|
using: dependencies
|
|
)
|
|
|
|
// Kick off a job to download the display picture
|
|
if
|
|
let url: String = displayPictureUrl,
|
|
let key: Data = displayPictureEncryptionKey
|
|
{
|
|
dependencies[singleton: .jobRunner].add(
|
|
db,
|
|
job: Job(
|
|
variant: .displayPictureDownload,
|
|
shouldBeUnique: true,
|
|
details: DisplayPictureDownloadJob.Details(
|
|
target: .profile(id: userSessionId.hexString, url: url, encryptionKey: key),
|
|
timestamp: profileLastUpdateTimestamp
|
|
)
|
|
),
|
|
canStartJob: dependencies[singleton: .appContext].isMainApp
|
|
)
|
|
}
|
|
|
|
// Extract the 'Note to Self' conversation settings
|
|
let targetPriority: Int32 = user_profile_get_nts_priority(conf)
|
|
let targetExpiry: Int32 = user_profile_get_nts_expiry(conf)
|
|
let targetIsEnable: Bool = targetExpiry > 0
|
|
let targetConfig: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration(
|
|
threadId: userSessionId.hexString,
|
|
isEnabled: targetIsEnable,
|
|
durationSeconds: TimeInterval(targetExpiry),
|
|
type: targetIsEnable ? .disappearAfterSend : .unknown
|
|
)
|
|
|
|
// The 'Note to Self' conversation should always exist so all we need to do is create/update
|
|
// it to match the desired state
|
|
_ = try SessionThread.upsert(
|
|
db,
|
|
id: userSessionId.hexString,
|
|
variant: .contact,
|
|
values: SessionThread.TargetValues(
|
|
shouldBeVisible: .setTo(LibSession.shouldBeVisible(priority: targetPriority)),
|
|
pinnedPriority: .setTo(targetPriority),
|
|
disappearingMessagesConfig: .setTo(targetConfig)
|
|
),
|
|
using: dependencies
|
|
)
|
|
|
|
// Notify of settings change if needed
|
|
let checkForCommunityMessageRequestsKey: ObservableKey = .setting(.checkForCommunityMessageRequests)
|
|
let oldCheckForCommunityMessageRequests: Bool? = oldState[checkForCommunityMessageRequestsKey] as? Bool
|
|
let newCheckForCommunityMessageRequests: Bool = get(.checkForCommunityMessageRequests)
|
|
|
|
if
|
|
oldCheckForCommunityMessageRequests != nil &&
|
|
oldCheckForCommunityMessageRequests != newCheckForCommunityMessageRequests
|
|
{
|
|
db.addEvent(newCheckForCommunityMessageRequests, forKey: checkForCommunityMessageRequestsKey)
|
|
}
|
|
|
|
// Create a contact for the current user if needed (also force-approve the current user
|
|
// in case the account got into a weird state or restored directly from a migration)
|
|
let userContact: Contact = Contact.fetchOrCreate(db, id: userSessionId.hexString, using: dependencies)
|
|
|
|
if !userContact.isTrusted || !userContact.isApproved || !userContact.didApproveMe {
|
|
try userContact.upsert(db)
|
|
try Contact
|
|
.filter(id: userSessionId.hexString)
|
|
.updateAllAndConfig(
|
|
db,
|
|
Contact.Columns.isTrusted.set(to: true), // Always trust the current user
|
|
Contact.Columns.isApproved.set(to: true),
|
|
Contact.Columns.didApproveMe.set(to: true),
|
|
using: dependencies
|
|
)
|
|
|
|
db.addContactEvent(id: userSessionId.hexString, change: .isTrusted(true))
|
|
db.addContactEvent(id: userSessionId.hexString, change: .isApproved(true))
|
|
db.addContactEvent(id: userSessionId.hexString, change: .didApproveMe(true))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Outgoing Changes
|
|
|
|
internal extension LibSession {
|
|
static func updateNoteToSelf(
|
|
priority: Int32? = nil,
|
|
disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil,
|
|
in config: Config?
|
|
) throws {
|
|
guard case .userProfile(let conf) = config else {
|
|
throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: config)
|
|
}
|
|
|
|
if let priority: Int32 = priority {
|
|
user_profile_set_nts_priority(conf, priority)
|
|
}
|
|
|
|
if let config: DisappearingMessagesConfiguration = disappearingMessagesConfig {
|
|
user_profile_set_nts_expiry(conf, Int32(config.durationSeconds))
|
|
}
|
|
}
|
|
|
|
static func updateSettings(
|
|
checkForCommunityMessageRequests: Bool? = nil,
|
|
in config: Config?
|
|
) throws {
|
|
guard case .userProfile(let conf) = config else {
|
|
throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: config)
|
|
}
|
|
|
|
if let blindedMessageRequests: Bool = checkForCommunityMessageRequests {
|
|
user_profile_set_blinded_msgreqs(conf, (blindedMessageRequests ? 1 : 0))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - External Outgoing Changes
|
|
|
|
public extension LibSession {
|
|
static func updateNoteToSelf(
|
|
_ db: ObservingDatabase,
|
|
priority: Int32? = nil,
|
|
disappearingMessagesConfig: DisappearingMessagesConfiguration? = nil,
|
|
using dependencies: Dependencies
|
|
) throws {
|
|
try dependencies.mutate(cache: .libSession) { cache in
|
|
try cache.performAndPushChange(db, for: .userProfile, sessionId: dependencies[cache: .general].sessionId) { config in
|
|
try LibSession.updateNoteToSelf(
|
|
priority: priority,
|
|
disappearingMessagesConfig: disappearingMessagesConfig,
|
|
in: config
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - State Access
|
|
|
|
public extension LibSession.Cache {
|
|
var displayName: String? {
|
|
guard
|
|
case .userProfile(let conf) = config(for: .userProfile, sessionId: userSessionId),
|
|
let profileNamePtr: UnsafePointer<CChar> = user_profile_get_name(conf)
|
|
else { return nil }
|
|
|
|
return String(cString: profileNamePtr)
|
|
}
|
|
|
|
func updateProfile(
|
|
displayName: Update<String>,
|
|
displayPictureUrl: Update<String?>,
|
|
displayPictureEncryptionKey: Update<Data?>,
|
|
isReuploadProfilePicture: Bool
|
|
) throws {
|
|
guard let config: LibSession.Config = config(for: .userProfile, sessionId: userSessionId) else {
|
|
throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: nil)
|
|
}
|
|
guard case .userProfile(let conf) = config else {
|
|
throw LibSessionError.invalidConfigObject(wanted: .userProfile, got: config)
|
|
}
|
|
|
|
/// Get the old values to determine if something changed
|
|
let oldName: String? = user_profile_get_name(conf).map { String(cString: $0) }
|
|
let oldNameFallback: String = (oldName ?? "")
|
|
let oldDisplayPic: user_profile_pic = user_profile_get_pic(conf)
|
|
let oldDisplayPictureUrl: String? = oldDisplayPic.get(\.url, nullIfEmpty: true)
|
|
let oldDisplayPictureKey: Data? = oldDisplayPic.get(\.key, nullIfEmpty: true)
|
|
|
|
/// Either assign the updated profile pic, or sent a blank profile pic (to remove the current one)
|
|
///
|
|
/// **Note:** We **MUST** update the profile picture first because doing so will result in any subsequent profile changes
|
|
/// which impact the `profile_updated` timestamp being routed to the "reupload" storage instead of the "standard"
|
|
/// storage - if we don't do this first then the "standard" timestamp will also get updated which can result in both timestamps
|
|
/// matching (in which case the "standard" profile wins and the re-uploaded content would be ignored)
|
|
if displayPictureUrl.or(oldDisplayPictureUrl) != oldDisplayPictureUrl {
|
|
var profilePic: user_profile_pic = user_profile_pic()
|
|
profilePic.set(\.url, to: displayPictureUrl.or(oldDisplayPictureUrl))
|
|
profilePic.set(\.key, to: displayPictureEncryptionKey.or(oldDisplayPictureKey))
|
|
|
|
switch isReuploadProfilePicture {
|
|
case true: user_profile_set_reupload_pic(conf, profilePic)
|
|
case false: user_profile_set_pic(conf, profilePic)
|
|
}
|
|
|
|
try LibSessionError.throwIfNeeded(conf)
|
|
|
|
/// Add a pending observation to notify any observers of the change once it's committed
|
|
addEvent(
|
|
key: .profile(userSessionId.hexString),
|
|
value: ProfileEvent(
|
|
id: userSessionId.hexString,
|
|
change: .displayPictureUrl(displayPictureUrl.or(oldDisplayPictureUrl))
|
|
)
|
|
)
|
|
}
|
|
|
|
/// Update the nam
|
|
///
|
|
/// **Note:** Setting the name (even if it hasn't changed) currently results in a timestamp change so only do this if it was
|
|
/// changed (this will be fixed in `libSession v1.5.8`)
|
|
if displayName.or("") != oldName {
|
|
var cUpdatedName: [CChar] = try displayName.or(oldNameFallback).cString(using: .utf8) ?? {
|
|
throw LibSessionError.invalidCConversion
|
|
}()
|
|
user_profile_set_name(conf, &cUpdatedName)
|
|
try LibSessionError.throwIfNeeded(conf)
|
|
|
|
/// Add a pending observation to notify any observers of the change once it's committed
|
|
addEvent(
|
|
key: .profile(userSessionId.hexString),
|
|
value: ProfileEvent(id: userSessionId.hexString, change: .name(displayName.or(oldNameFallback)))
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - ProfileInfo
|
|
|
|
public extension LibSession {
|
|
struct ProfileInfo {
|
|
let name: String
|
|
let profilePictureUrl: String?
|
|
let profileEncryptionKey: Data?
|
|
}
|
|
}
|
|
|
|
// MARK: - C Conformance
|
|
|
|
extension user_profile_pic: CAccessible & CMutable {}
|