mirror of https://github.com/oxen-io/session-ios
Started adding migration logic for contacts
Updated the getUserHexEncodedPublicKey to take an optional db value so we can retrieve it during the initial migrationpull/612/head
parent
72eeb1c796
commit
4ee4b3ffb3
@ -1,127 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
@objc(SNContact)
|
||||
public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
@objc public let sessionID: String
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
@objc public var profilePictureURL: String?
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
@objc public var profilePictureFileName: String?
|
||||
/// The key with which the profile is encrypted.
|
||||
@objc public var profileEncryptionKey: OWSAES256Key?
|
||||
/// The ID of the thread associated with this contact.
|
||||
@objc public var threadID: String?
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
@objc public var isTrusted = false
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
@objc public var isApproved = false
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
@objc public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
@objc public var didApproveMe = false
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
@objc public var hasBeenBlocked = false
|
||||
|
||||
// MARK: Name
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
@objc public var name: String?
|
||||
/// The contact's nickname, if the user set one.
|
||||
@objc public var nickname: String?
|
||||
/// The name to display in the UI. For local use only.
|
||||
@objc public func displayName(for context: Context) -> String? {
|
||||
if let nickname = nickname { return nickname }
|
||||
switch context {
|
||||
case .regular: return name
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
guard let name = name else { return nil }
|
||||
let endIndex = sessionID.endIndex
|
||||
let cutoffIndex = sessionID.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(sessionID[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Context
|
||||
@objc(SNContactContext)
|
||||
public enum Context : Int {
|
||||
case regular, openGroup
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public init(sessionID: String) {
|
||||
self.sessionID = sessionID
|
||||
super.init()
|
||||
}
|
||||
|
||||
private override init() { preconditionFailure("Use init(sessionID:) instead.") }
|
||||
|
||||
// MARK: Validation
|
||||
public var isValid: Bool {
|
||||
if profilePictureURL != nil { return (profileEncryptionKey != nil) }
|
||||
if profileEncryptionKey != nil { return (profilePictureURL != nil) }
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String? else { return nil }
|
||||
self.sessionID = sessionID
|
||||
isTrusted = coder.decodeBool(forKey: "isTrusted")
|
||||
if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name }
|
||||
if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
|
||||
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
|
||||
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
|
||||
|
||||
let isBlockedFlag: Bool = coder.decodeBool(forKey: "isBlocked")
|
||||
isApproved = coder.decodeBool(forKey: "isApproved")
|
||||
isBlocked = isBlockedFlag
|
||||
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
|
||||
hasBeenBlocked = (coder.decodeBool(forKey: "hasBeenBlocked") || isBlockedFlag)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(sessionID, forKey: "sessionID")
|
||||
coder.encode(name, forKey: "displayName")
|
||||
coder.encode(nickname, forKey: "nickname")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profilePictureFileName, forKey: "profilePictureFileName")
|
||||
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(isTrusted, forKey: "isTrusted")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
coder.encode(hasBeenBlocked, forKey: "hasBeenBlocked")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? Contact else { return false }
|
||||
return sessionID == other.sessionID
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return sessionID.hash
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String {
|
||||
nickname ?? name ?? sessionID
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
@objc(contextForThread:)
|
||||
public static func context(for thread: TSThread) -> Context {
|
||||
return ((thread as? TSGroupThread)?.isOpenGroup == true) ? .openGroup : .regular
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "contact" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
|
||||
case isTrusted
|
||||
case isApproved
|
||||
case isBlocked
|
||||
case didApproveMe
|
||||
case hasBeenBlocked
|
||||
}
|
||||
|
||||
/// The id for the contact (Note: This could be a sessionId, a blindedId or some future variant)
|
||||
public let id: String
|
||||
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
public var isTrusted = false
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
public var isApproved = false
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
public var didApproveMe = false
|
||||
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
public var hasBeenBlocked = false
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public static var databaseTableName: String { "profile" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
|
||||
case name = "displayName"
|
||||
case nickname
|
||||
|
||||
case profilePictureUrl = "profilePictureURL"
|
||||
case profilePictureFileName
|
||||
case profileEncryptionKey
|
||||
}
|
||||
|
||||
/// 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 var name: String
|
||||
|
||||
/// A custom name for the profile set by the current user
|
||||
public var nickname: String?
|
||||
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
public var profilePictureUrl: String?
|
||||
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
public var profilePictureFileName: String?
|
||||
|
||||
/// The key with which the profile is encrypted.
|
||||
public var profileEncryptionKey: OWSAES256Key?
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public var description: String {
|
||||
"""
|
||||
Profile(
|
||||
displayName: \(name),
|
||||
profileKey: \(profileEncryptionKey?.keyData.description ?? "null"),
|
||||
profilePictureURL: \(profilePictureUrl ?? "null")
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public extension Profile {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
var profileKey: OWSAES256Key?
|
||||
var profilePictureUrl: String?
|
||||
|
||||
// If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
|
||||
if
|
||||
let profileKeyData: Data = try? container.decode(Data.self, forKey: .profileEncryptionKey),
|
||||
let profilePictureUrlValue: String = try? container.decode(String.self, forKey: .profilePictureUrl)
|
||||
{
|
||||
guard let validProfileKey: OWSAES256Key = OWSAES256Key(data: profileKeyData) else {
|
||||
owsFailDebug("Failed to make profile key for key data")
|
||||
throw GRDBStorageError.decodingFailed
|
||||
}
|
||||
|
||||
profileKey = validProfileKey
|
||||
profilePictureUrl = profilePictureUrlValue
|
||||
}
|
||||
|
||||
self = Profile(
|
||||
id: try container.decode(String.self, forKey: .id),
|
||||
name: try container.decode(String.self, forKey: .name),
|
||||
nickname: try? container.decode(String.self, forKey: .nickname),
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName),
|
||||
profileEncryptionKey: profileKey
|
||||
)
|
||||
}
|
||||
|
||||
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.encode(nickname, forKey: .nickname)
|
||||
try container.encode(profilePictureUrl, forKey: .profilePictureUrl)
|
||||
try container.encode(profilePictureFileName, forKey: .profilePictureFileName)
|
||||
try container.encode(profileEncryptionKey?.keyData, forKey: .profileEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protobuf
|
||||
|
||||
public extension Profile {
|
||||
static func fromProto(_ proto: SNProtoDataMessage, id: String) -> Profile? {
|
||||
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
|
||||
|
||||
var profileKey: OWSAES256Key?
|
||||
var profilePictureUrl: String?
|
||||
|
||||
// If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
|
||||
if let profileKeyData: Data = proto.profileKey, profileProto.profilePicture != nil {
|
||||
guard let validProfileKey: OWSAES256Key = OWSAES256Key(data: profileKeyData) else {
|
||||
owsFailDebug("Failed to make profile key for key data")
|
||||
return nil
|
||||
}
|
||||
|
||||
profileKey = validProfileKey
|
||||
profilePictureUrl = profileProto.profilePicture
|
||||
}
|
||||
|
||||
return Profile(
|
||||
id: id,
|
||||
name: displayName,
|
||||
nickname: nil,
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profilePictureFileName: nil,
|
||||
profileEncryptionKey: profileKey
|
||||
)
|
||||
}
|
||||
|
||||
func toProto() -> SNProtoDataMessage? {
|
||||
let dataMessageProto = SNProtoDataMessage.builder()
|
||||
let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
profileProto.setDisplayName(name)
|
||||
|
||||
if let profileKey: OWSAES256Key = profileEncryptionKey, let profilePictureUrl: String = profilePictureUrl {
|
||||
dataMessageProto.setProfileKey(profileKey.keyData)
|
||||
profileProto.setProfilePicture(profilePictureUrl)
|
||||
}
|
||||
|
||||
do {
|
||||
dataMessageProto.setProfile(try profileProto.build())
|
||||
return try dataMessageProto.build()
|
||||
}
|
||||
catch {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public extension Profile {
|
||||
// MARK: - Context
|
||||
|
||||
enum Context: Int {
|
||||
case regular
|
||||
case openGroup
|
||||
}
|
||||
|
||||
/// The name to display in the UI. For local use only.
|
||||
func displayName(for context: Context) -> String? {
|
||||
if let nickname: String = nickname { return nickname }
|
||||
|
||||
switch context {
|
||||
case .regular: return name
|
||||
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
let endIndex = id.endIndex
|
||||
let cutoffIndex = id.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(id[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue