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/Messages/Control Messages/ConfigurationMessage.swift

369 lines
15 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import GRDB
import Curve25519Kit
import SessionUtilitiesKit
public final class ConfigurationMessage: ControlMessage {
private enum CodingKeys: String, CodingKey {
case closedGroups
case openGroups
case displayName
case profilePictureUrl
case profileKey
case contacts
}
public var closedGroups: Set<CMClosedGroup> = []
public var openGroups: Set<String> = []
public var displayName: String?
public var profilePictureUrl: String?
public var profileKey: Data?
public var contacts: Set<CMContact> = []
public override var isSelfSendValid: Bool { true }
// MARK: - Initialization
public init(
displayName: String?,
profilePictureUrl: String?,
profileKey: Data?,
closedGroups: Set<CMClosedGroup>,
openGroups: Set<String>,
contacts: Set<CMContact>
) {
super.init()
self.displayName = displayName
self.profilePictureUrl = profilePictureUrl
self.profileKey = profileKey
self.closedGroups = closedGroups
self.openGroups = openGroups
self.contacts = contacts
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
closedGroups = ((try? container.decode(Set<CMClosedGroup>.self, forKey: .closedGroups)) ?? [])
openGroups = ((try? container.decode(Set<String>.self, forKey: .openGroups)) ?? [])
displayName = try? container.decode(String.self, forKey: .displayName)
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
profileKey = try? container.decode(Data.self, forKey: .profileKey)
contacts = ((try? container.decode(Set<CMContact>.self, forKey: .contacts)) ?? [])
}
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(closedGroups, forKey: .closedGroups)
try container.encodeIfPresent(openGroups, forKey: .openGroups)
try container.encodeIfPresent(displayName, forKey: .displayName)
try container.encodeIfPresent(profilePictureUrl, forKey: .profilePictureUrl)
try container.encodeIfPresent(profileKey, forKey: .profileKey)
try container.encodeIfPresent(contacts, forKey: .contacts)
}
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ConfigurationMessage? {
guard let configurationProto = proto.configurationMessage else { return nil }
let displayName = configurationProto.displayName
let profilePictureUrl = configurationProto.profilePicture
let profileKey = configurationProto.profileKey
let closedGroups = Set(configurationProto.closedGroups.compactMap { CMClosedGroup.fromProto($0) })
let openGroups = Set(configurationProto.openGroups)
let contacts = Set(configurationProto.contacts.compactMap { CMContact.fromProto($0) })
return ConfigurationMessage(
displayName: displayName,
profilePictureUrl: profilePictureUrl,
profileKey: profileKey,
closedGroups: closedGroups,
openGroups: openGroups,
contacts: contacts
)
}
public override func toProto(_ db: Database) -> SNProtoContent? {
let configurationProto = SNProtoConfigurationMessage.builder()
if let displayName = displayName { configurationProto.setDisplayName(displayName) }
if let profilePictureUrl = profilePictureUrl { configurationProto.setProfilePicture(profilePictureUrl) }
if let profileKey = profileKey { configurationProto.setProfileKey(profileKey) }
configurationProto.setClosedGroups(closedGroups.compactMap { $0.toProto() })
configurationProto.setOpenGroups([String](openGroups))
configurationProto.setContacts(contacts.compactMap { $0.toProto() })
let contentProto = SNProtoContent.builder()
do {
contentProto.setConfigurationMessage(try configurationProto.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct configuration proto from: \(self).")
return nil
}
}
// MARK: - Description
public var description: String {
"""
ConfigurationMessage(
closedGroups: \([CMClosedGroup](closedGroups).prettifiedDescription),
openGroups: \([String](openGroups).prettifiedDescription),
displayName: \(displayName ?? "null"),
profilePictureUrl: \(profilePictureUrl ?? "null"),
profileKey: \(profileKey?.toHexString() ?? "null"),
contacts: \([CMContact](contacts).prettifiedDescription)
)
"""
}
}
// MARK: - Closed Group
extension ConfigurationMessage {
public struct CMClosedGroup: Codable, Hashable, CustomStringConvertible {
private enum CodingKeys: String, CodingKey {
case publicKey
case name
case encryptionKeyPublicKey
case encryptionKeySecretKey
case members
case admins
case expirationTimer
}
public let publicKey: String
public let name: String
public let encryptionKeyPublicKey: Data
public let encryptionKeySecretKey: Data
public let members: Set<String>
public let admins: Set<String>
public let expirationTimer: UInt32
public var isValid: Bool { !members.isEmpty && !admins.isEmpty }
// MARK: - Initialization
public init(
publicKey: String,
name: String,
encryptionKeyPublicKey: Data,
encryptionKeySecretKey: Data,
members: Set<String>,
admins: Set<String>,
expirationTimer: UInt32
) {
self.publicKey = publicKey
self.name = name
self.encryptionKeyPublicKey = encryptionKeyPublicKey
self.encryptionKeySecretKey = encryptionKeySecretKey
self.members = members
self.admins = admins
self.expirationTimer = expirationTimer
}
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
publicKey = try container.decode(String.self, forKey: .publicKey)
name = try container.decode(String.self, forKey: .name)
encryptionKeyPublicKey = try container.decode(Data.self, forKey: .encryptionKeyPublicKey)
encryptionKeySecretKey = try container.decode(Data.self, forKey: .encryptionKeySecretKey)
members = try container.decode(Set<String>.self, forKey: .members)
admins = try container.decode(Set<String>.self, forKey: .admins)
expirationTimer = try container.decode(UInt32.self, forKey: .expirationTimer)
}
public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(publicKey, forKey: .publicKey)
try container.encode(name, forKey: .name)
try container.encode(encryptionKeyPublicKey, forKey: .encryptionKeyPublicKey)
try container.encode(encryptionKeySecretKey, forKey: .encryptionKeySecretKey)
try container.encode(members, forKey: .members)
try container.encode(admins, forKey: .admins)
try container.encode(expirationTimer, forKey: .expirationTimer)
}
public static func fromProto(_ proto: SNProtoConfigurationMessageClosedGroup) -> CMClosedGroup? {
guard
let publicKey = proto.publicKey?.toHexString(),
let name = proto.name,
let encryptionKeyPairAsProto = proto.encryptionKeyPair
else { return nil }
let members = Set(proto.members.map { $0.toHexString() })
let admins = Set(proto.admins.map { $0.toHexString() })
let expirationTimer = proto.expirationTimer
let result = CMClosedGroup(
publicKey: publicKey,
name: name,
encryptionKeyPublicKey: encryptionKeyPairAsProto.publicKey,
encryptionKeySecretKey: encryptionKeyPairAsProto.privateKey,
members: members,
admins: admins,
expirationTimer: expirationTimer
)
guard result.isValid else { return nil }
return result
}
public func toProto() -> SNProtoConfigurationMessageClosedGroup? {
guard isValid else { return nil }
let result = SNProtoConfigurationMessageClosedGroup.builder()
result.setPublicKey(Data(hex: publicKey))
result.setName(name)
do {
let encryptionKeyPairAsProto = try SNProtoKeyPair.builder(
publicKey: encryptionKeyPublicKey,
privateKey: encryptionKeySecretKey
).build()
result.setEncryptionKeyPair(encryptionKeyPairAsProto)
} catch {
SNLog("Couldn't construct closed group proto from: \(self).")
return nil
}
result.setMembers(members.map { Data(hex: $0) })
result.setAdmins(admins.map { Data(hex: $0) })
result.setExpirationTimer(expirationTimer)
do {
return try result.build()
} catch {
SNLog("Couldn't construct closed group proto from: \(self).")
return nil
}
}
public var description: String { name }
}
}
// MARK: - Contact
extension ConfigurationMessage {
public struct CMContact: Codable, Hashable, CustomStringConvertible {
private enum CodingKeys: String, CodingKey {
case publicKey
case displayName
case profilePictureUrl
case profileKey
case hasIsApproved
case isApproved
case hasIsBlocked
case isBlocked
case hasDidApproveMe
case didApproveMe
}
public var publicKey: String?
public var displayName: String?
public var profilePictureUrl: String?
public var profileKey: Data?
public var hasIsApproved: Bool
public var isApproved: Bool
public var hasIsBlocked: Bool
public var isBlocked: Bool
public var hasDidApproveMe: Bool
public var didApproveMe: Bool
public var isValid: Bool { publicKey != nil && displayName != nil }
public init(
publicKey: String?,
displayName: String?,
profilePictureUrl: String?,
profileKey: Data?,
hasIsApproved: Bool,
isApproved: Bool,
hasIsBlocked: Bool,
isBlocked: Bool,
hasDidApproveMe: Bool,
didApproveMe: Bool
) {
self.publicKey = publicKey
self.displayName = displayName
self.profilePictureUrl = profilePictureUrl
self.profileKey = profileKey
self.hasIsApproved = hasIsApproved
self.isApproved = isApproved
self.hasIsBlocked = hasIsBlocked
self.isBlocked = isBlocked
self.hasDidApproveMe = hasDidApproveMe
self.didApproveMe = didApproveMe
}
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
publicKey = try? container.decode(String.self, forKey: .publicKey)
displayName = try? container.decode(String.self, forKey: .displayName)
profilePictureUrl = try? container.decode(String.self, forKey: .profilePictureUrl)
profileKey = try? container.decode(Data.self, forKey: .profileKey)
hasIsApproved = try container.decode(Bool.self, forKey: .hasIsApproved)
isApproved = try container.decode(Bool.self, forKey: .isApproved)
hasIsBlocked = try container.decode(Bool.self, forKey: .hasIsBlocked)
isBlocked = try container.decode(Bool.self, forKey: .isBlocked)
hasDidApproveMe = try container.decode(Bool.self, forKey: .hasDidApproveMe)
didApproveMe = try container.decode(Bool.self, forKey: .didApproveMe)
}
public static func fromProto(_ proto: SNProtoConfigurationMessageContact) -> CMContact? {
let result: CMContact = CMContact(
publicKey: proto.publicKey.toHexString(),
displayName: proto.name,
profilePictureUrl: proto.profilePicture,
profileKey: proto.profileKey,
hasIsApproved: proto.hasIsApproved,
isApproved: proto.isApproved,
hasIsBlocked: proto.hasIsBlocked,
isBlocked: proto.isBlocked,
hasDidApproveMe: proto.hasDidApproveMe,
didApproveMe: proto.didApproveMe
)
guard result.isValid else { return nil }
return result
}
public func toProto() -> SNProtoConfigurationMessageContact? {
guard isValid else { return nil }
guard let publicKey = publicKey, let displayName = displayName else { return nil }
let result = SNProtoConfigurationMessageContact.builder(publicKey: Data(hex: publicKey), name: displayName)
if let profilePictureUrl = profilePictureUrl { result.setProfilePicture(profilePictureUrl) }
if let profileKey = profileKey { result.setProfileKey(profileKey) }
if hasIsApproved { result.setIsApproved(isApproved) }
if hasIsBlocked { result.setIsBlocked(isBlocked) }
if hasDidApproveMe { result.setDidApproveMe(didApproveMe) }
do {
return try result.build()
} catch {
SNLog("Couldn't construct contact proto from: \(self).")
return nil
}
}
public var description: String { displayName ?? "" }
}
}