Files
Morgan Pretty 03df067dc8 Fixed some build errors, resolved some TODOs, fixed a couple of bugs
• Added logic to handle non-originating accounts
• Added explicit SessionPro errors
• Moved profile pro decisions into the SessionProManager to consolidate the logic
• Resolved a bunch of TODOs
• Fixed an issue where a invalid pro key would prevent messages from decoding
2025-12-17 13:10:34 +11:00

416 lines
16 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import DifferenceKit
import SessionUIKit
import SessionUtilitiesKit
/// This type is duplicate in both the database and within the LibSession config so should only ever have it's data changes via the
/// `updateAllAndConfig` function. Updating it elsewhere could result in issues with syncing data between devices
public struct Profile: Codable, Sendable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Differentiable {
public static var databaseTableName: String { "profile" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case id
case name
case nickname
case displayPictureUrl
case displayPictureEncryptionKey
case profileLastUpdated
case blocksCommunityMessageRequests
case proFeatures
case proExpiryUnixTimestampMs
case proGenIndexHashHex
}
/// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant)
public let id: String
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
public let name: String
/// A custom name for the profile set by the current user
public let nickname: String?
/// The URL from which to fetch the contact's profile picture
///
/// **Note:** This won't be updated until the display picture has actually been downloaded
public let displayPictureUrl: String?
/// The key with which the profile is encrypted.
public let displayPictureEncryptionKey: Data?
/// The timestamp (in seconds since epoch) that the profile was last updated
public let profileLastUpdated: TimeInterval?
/// A flag indicating whether this profile has reported that it blocks community message requests
public let blocksCommunityMessageRequests: Bool?
/// The Session Pro features enabled for this profile
public let proFeatures: SessionPro.ProfileFeatures
/// The unix timestamp (in milliseconds) when Session Pro expires for this profile
public let proExpiryUnixTimestampMs: UInt64
/// Hash of the generation index for this users Session Pro
public let proGenIndexHashHex: String?
// MARK: - Initialization
public static func with(
id: String,
name: String,
nickname: String? = nil,
displayPictureUrl: String? = nil,
displayPictureEncryptionKey: Data? = nil,
profileLastUpdated: TimeInterval? = nil,
blocksCommunityMessageRequests: Bool? = nil,
proFeatures: SessionPro.ProfileFeatures = .none,
proExpiryUnixTimestampMs: UInt64 = 0,
proGenIndexHashHex: String? = nil
) -> Profile {
return Profile(
id: id,
name: name,
nickname: nickname,
displayPictureUrl: displayPictureUrl,
displayPictureEncryptionKey: displayPictureEncryptionKey,
profileLastUpdated: profileLastUpdated,
blocksCommunityMessageRequests: blocksCommunityMessageRequests,
proFeatures: proFeatures,
proExpiryUnixTimestampMs: proExpiryUnixTimestampMs,
proGenIndexHashHex: proGenIndexHashHex
)
}
}
// MARK: - Description
extension Profile: CustomStringConvertible, CustomDebugStringConvertible {
public var description: String {
"""
Profile(
name: \(name),
profileKey: \(displayPictureEncryptionKey?.description ?? "null"),
profilePictureUrl: \(displayPictureUrl ?? "null")
)
"""
}
public var debugDescription: String {
return """
Profile(
id: \(id),
name: \(name),
nickname: \(nickname.map { "\($0)" } ?? "null"),
displayPictureUrl: \(displayPictureUrl.map { "\"\($0)\"" } ?? "null"),
displayPictureEncryptionKey: \(displayPictureEncryptionKey?.toHexString() ?? "null"),
profileLastUpdated: \(profileLastUpdated.map { "\($0)" } ?? "null"),
blocksCommunityMessageRequests: \(blocksCommunityMessageRequests.map { "\($0)" } ?? "null"),
proFeatures: \(proFeatures),
proExpiryUnixTimestampMs: \(proExpiryUnixTimestampMs),
proGenIndexHashHex: \(proGenIndexHashHex.map { "\($0)" } ?? "null")
)
"""
}
}
// MARK: - Codable
public extension Profile {
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
var displayPictureKey: Data?
var displayPictureUrl: String?
// If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
if
let displayPictureKeyData: Data = try? container.decode(Data?.self, forKey: .displayPictureEncryptionKey),
let displayPictureUrlValue: String = try? container.decode(String?.self, forKey: .displayPictureUrl)
{
displayPictureKey = displayPictureKeyData
displayPictureUrl = displayPictureUrlValue
}
self = Profile(
id: try container.decode(String.self, forKey: .id),
name: try container.decode(String.self, forKey: .name),
nickname: try container.decodeIfPresent(String.self, forKey: .nickname),
displayPictureUrl: displayPictureUrl,
displayPictureEncryptionKey: displayPictureKey,
profileLastUpdated: try container.decodeIfPresent(TimeInterval.self, forKey: .profileLastUpdated),
blocksCommunityMessageRequests: try container.decodeIfPresent(Bool.self, forKey: .blocksCommunityMessageRequests),
proFeatures: try container.decode(SessionPro.ProfileFeatures.self, forKey: .proFeatures),
proExpiryUnixTimestampMs: try container.decode(UInt64.self, forKey: .proExpiryUnixTimestampMs),
proGenIndexHashHex: try container.decodeIfPresent(String.self, forKey: .proGenIndexHashHex)
)
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encodeIfPresent(nickname, forKey: .nickname)
try container.encodeIfPresent(displayPictureUrl, forKey: .displayPictureUrl)
try container.encodeIfPresent(displayPictureEncryptionKey, forKey: .displayPictureEncryptionKey)
try container.encodeIfPresent(profileLastUpdated, forKey: .profileLastUpdated)
try container.encodeIfPresent(blocksCommunityMessageRequests, forKey: .blocksCommunityMessageRequests)
try container.encode(proFeatures, forKey: .proFeatures)
try container.encode(proExpiryUnixTimestampMs, forKey: .proExpiryUnixTimestampMs)
try container.encodeIfPresent(proGenIndexHashHex, forKey: .proGenIndexHashHex)
}
}
// MARK: - Protobuf
public extension Profile {
func toProto() -> SNProtoDataMessage? {
let dataMessageProto = SNProtoDataMessage.builder()
let profileProto = SNProtoLokiProfile.builder()
profileProto.setDisplayName(name)
if
let displayPictureEncryptionKey: Data = displayPictureEncryptionKey,
let displayPictureUrl: String = displayPictureUrl
{
dataMessageProto.setProfileKey(displayPictureEncryptionKey)
profileProto.setProfilePicture(displayPictureUrl)
}
if let profileLastUpdated: TimeInterval = profileLastUpdated {
profileProto.setLastUpdateSeconds(UInt64(profileLastUpdated))
}
do {
dataMessageProto.setProfile(try profileProto.build())
return try dataMessageProto.build()
}
catch {
Log.warn(.messageSender, "Couldn't construct profile proto from: \(self).")
return nil
}
}
}
// MARK: - GRDB Interactions
public extension Profile {
static func displayName(
_ db: ObservingDatabase,
id: ID,
customFallback: String? = nil
) -> String {
let existingDisplayName: String? = (try? Profile.fetchOne(db, id: id))?.displayName()
return (existingDisplayName ?? (customFallback ?? id))
}
static func displayNameNoFallback(
_ db: ObservingDatabase,
id: ID,
threadVariant: SessionThread.Variant = .contact,
suppressId: Bool = false
) -> String? {
return (try? Profile.fetchOne(db, id: id))?.displayName()
}
// MARK: - Fetch or Create
static func defaultFor(_ id: String) -> Profile {
return Profile(
id: id,
name: "",
nickname: nil,
displayPictureUrl: nil,
displayPictureEncryptionKey: nil,
profileLastUpdated: nil,
blocksCommunityMessageRequests: nil,
proFeatures: .none,
proExpiryUnixTimestampMs: 0,
proGenIndexHashHex: nil
)
}
/// Fetches or creates a Profile for the specified user
///
/// **Note:** This method intentionally does **not** save the newly created Profile,
/// it will need to be explicitly saved after calling
static func fetchOrCreate(_ db: ObservingDatabase, id: String) -> Profile {
return (
(try? Profile.fetchOne(db, id: id)) ??
defaultFor(id)
)
}
}
// MARK: - Search Queries
public extension Profile {
struct FullTextSearch: Decodable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case nickname
case name
}
let nickname: String?
let name: String
}
}
// MARK: - Convenience
public extension Profile {
/// The name to display in the UI for a given thread variant
func displayName(
messageProfile: VisibleMessage.VMProfile? = nil,
ignoreNickname: Bool = false,
showYouForCurrentUser: Bool = true,
currentUserSessionIds: Set<String> = [],
includeSessionIdSuffix: Bool = false
) -> String {
return Profile.displayName(
id: id,
name: (messageProfile?.displayName?.nullIfEmpty ?? name),
nickname: (ignoreNickname ? nil : nickname),
showYouForCurrentUser: showYouForCurrentUser,
currentUserSessionIds: currentUserSessionIds,
includeSessionIdSuffix: includeSessionIdSuffix
)
}
static func displayName(
id: String,
name: String?,
nickname: String?,
showYouForCurrentUser: Bool = true,
currentUserSessionIds: Set<String> = [],
includeSessionIdSuffix: Bool = false,
customFallback: String? = nil
) -> String {
if showYouForCurrentUser && currentUserSessionIds.contains(id) {
return "you".localized()
}
// stringlint:ignore_contents
switch (nickname, name, customFallback, includeSessionIdSuffix) {
case (.some(let value), _, _, false) where !value.isEmpty && value != id,
(_, .some(let value), _, false) where !value.isEmpty && value != id,
(_, _, .some(let value), false) where !value.isEmpty && value != id:
return value
case (.some(let value), _, _, true) where !value.isEmpty && value != id,
(_, .some(let value), _, true) where !value.isEmpty && value != id,
(_, _, .some(let value), true) where !value.isEmpty && value != id:
return (Dependencies.isRTL ?
"(\(id.truncated(prefix: 4, suffix: 4))) \(value)" :
"​\(value) (\(id.truncated(prefix: 4, suffix: 4)))"
)
default: return id.truncated(prefix: 4, suffix: 4)
}
}
}
// MARK: - WithProfile<T>
public struct WithProfile<T: ProfileAssociated>: Equatable, Hashable, Comparable {
public let value: T
public let profile: Profile?
public let currentUserSessionId: SessionId
public var profileId: String { value.profileId }
public init(value: T, profile: Profile?, currentUserSessionId: SessionId) {
self.value = value
self.profile = profile
self.currentUserSessionId = currentUserSessionId
}
public func itemDescription(using dependencies: Dependencies) -> String? {
return value.itemDescription(using: dependencies)
}
public func itemDescriptionColor(using dependencies: Dependencies) -> ThemeValue {
return value.itemDescriptionColor(using: dependencies)
}
public static func < (lhs: WithProfile<T>, rhs: WithProfile<T>) -> Bool {
return T.compare(lhs: lhs, rhs: rhs)
}
}
extension WithProfile: Differentiable where T: Differentiable {}
public protocol ProfileAssociated: Equatable, Hashable {
var profileId: String { get }
var profileIcon: ProfilePictureView.Info.ProfileIcon { get }
func itemDescription(using dependencies: Dependencies) -> String?
func itemDescriptionColor(using dependencies: Dependencies) -> ThemeValue
static func compare(lhs: WithProfile<Self>, rhs: WithProfile<Self>) -> Bool
}
public extension ProfileAssociated {
var profileIcon: ProfilePictureView.Info.ProfileIcon { return .none }
func itemDescription(using dependencies: Dependencies) -> String? { return nil }
func itemDescriptionColor(using dependencies: Dependencies) -> ThemeValue { return .textPrimary }
}
public extension FetchRequest where RowDecoder: FetchableRecord & ProfileAssociated {
func fetchAllWithProfiles(_ db: ObservingDatabase, using dependencies: Dependencies) throws -> [WithProfile<RowDecoder>] {
let originalResult: [RowDecoder] = try self.fetchAll(db)
let profiles: [String: Profile]? = try? Profile
.fetchAll(db, ids: originalResult.map { $0.profileId }.asSet())
.reduce(into: [:]) { result, next in result[next.id] = next }
return originalResult.map {
WithProfile(
value: $0,
profile: profiles?[$0.profileId],
currentUserSessionId: dependencies[cache: .general].sessionId
)
}
}
}
// MARK: - Convenience
public extension Profile {
func with(
name: String? = nil,
nickname: Update<String?> = .useExisting,
displayPictureUrl: Update<String?> = .useExisting,
displayPictureEncryptionKey: Update<Data?> = .useExisting,
profileLastUpdated: Update<TimeInterval?> = .useExisting,
blocksCommunityMessageRequests: Update<Bool?> = .useExisting,
proFeatures: Update<SessionPro.ProfileFeatures> = .useExisting,
proExpiryUnixTimestampMs: Update<UInt64> = .useExisting,
proGenIndexHashHex: Update<String?> = .useExisting
) -> Profile {
return Profile(
id: id,
name: (name ?? self.name),
nickname: nickname.or(self.nickname),
displayPictureUrl: displayPictureUrl.or(self.displayPictureUrl),
displayPictureEncryptionKey: displayPictureEncryptionKey.or(self.displayPictureEncryptionKey),
profileLastUpdated: profileLastUpdated.or(self.profileLastUpdated),
blocksCommunityMessageRequests: blocksCommunityMessageRequests.or(self.blocksCommunityMessageRequests),
proFeatures: proFeatures.or(self.proFeatures),
proExpiryUnixTimestampMs: proExpiryUnixTimestampMs.or(self.proExpiryUnixTimestampMs),
proGenIndexHashHex: proGenIndexHashHex.or(self.proGenIndexHashHex)
)
}
}