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