mirror of https://github.com/oxen-io/session-ios
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.
436 lines
19 KiB
Swift
436 lines
19 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import GRDB
|
|
import SessionUtilitiesKit
|
|
|
|
public final class ClosedGroupControlMessage: ControlMessage {
|
|
private enum CodingKeys: String, CodingKey {
|
|
case kind
|
|
}
|
|
|
|
public var kind: Kind?
|
|
|
|
public override var ttl: UInt64 {
|
|
switch kind {
|
|
case .encryptionKeyPair: return 14 * 24 * 60 * 60 * 1000
|
|
default: return 14 * 24 * 60 * 60 * 1000
|
|
}
|
|
}
|
|
|
|
public override var isSelfSendValid: Bool { true }
|
|
|
|
// MARK: - Kind
|
|
|
|
public enum Kind: CustomStringConvertible, Codable {
|
|
private enum CodingKeys: String, CodingKey {
|
|
case description
|
|
case publicKey
|
|
case name
|
|
case encryptionPublicKey
|
|
case encryptionSecretKey
|
|
case members
|
|
case admins
|
|
case expirationTimer
|
|
case wrappers
|
|
}
|
|
|
|
case new(publicKey: Data, name: String, encryptionKeyPair: KeyPair, members: [Data], admins: [Data], expirationTimer: UInt32)
|
|
|
|
/// An encryption key pair encrypted for each member individually.
|
|
///
|
|
/// - Note: `publicKey` is only set when an encryption key pair is sent in a one-to-one context (i.e. not in a group).
|
|
case encryptionKeyPair(publicKey: Data?, wrappers: [KeyPairWrapper])
|
|
case nameChange(name: String)
|
|
case membersAdded(members: [Data])
|
|
case membersRemoved(members: [Data])
|
|
case memberLeft
|
|
case encryptionKeyPairRequest
|
|
|
|
public var description: String {
|
|
switch self {
|
|
case .new: return "new" // stringlint:disable
|
|
case .encryptionKeyPair: return "encryptionKeyPair" // stringlint:disable
|
|
case .nameChange: return "nameChange" // stringlint:disable
|
|
case .membersAdded: return "membersAdded" // stringlint:disable
|
|
case .membersRemoved: return "membersRemoved" // stringlint:disable
|
|
case .memberLeft: return "memberLeft" // stringlint:disable
|
|
case .encryptionKeyPairRequest: return "encryptionKeyPairRequest" // stringlint:disable
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
// Compare the descriptions to find the appropriate case
|
|
let description: String = try container.decode(String.self, forKey: .description)
|
|
let newDescription: String = Kind.new(
|
|
publicKey: Data(),
|
|
name: "",
|
|
encryptionKeyPair: KeyPair(publicKey: [], secretKey: []),
|
|
members: [],
|
|
admins: [],
|
|
expirationTimer: 0
|
|
).description
|
|
|
|
switch description {
|
|
case newDescription:
|
|
self = .new(
|
|
publicKey: try container.decode(Data.self, forKey: .publicKey),
|
|
name: try container.decode(String.self, forKey: .name),
|
|
encryptionKeyPair: KeyPair(
|
|
publicKey: try container.decode([UInt8].self, forKey: .encryptionPublicKey),
|
|
secretKey: try container.decode([UInt8].self, forKey: .encryptionSecretKey)
|
|
),
|
|
members: try container.decode([Data].self, forKey: .members),
|
|
admins: try container.decode([Data].self, forKey: .admins),
|
|
expirationTimer: try container.decode(UInt32.self, forKey: .expirationTimer)
|
|
)
|
|
|
|
case Kind.encryptionKeyPair(publicKey: nil, wrappers: []).description:
|
|
self = .encryptionKeyPair(
|
|
publicKey: try? container.decode(Data.self, forKey: .publicKey),
|
|
wrappers: try container.decode([ClosedGroupControlMessage.KeyPairWrapper].self, forKey: .wrappers)
|
|
)
|
|
|
|
case Kind.nameChange(name: "").description:
|
|
self = .nameChange(
|
|
name: try container.decode(String.self, forKey: .name)
|
|
)
|
|
|
|
case Kind.membersAdded(members: []).description:
|
|
self = .membersAdded(
|
|
members: try container.decode([Data].self, forKey: .members)
|
|
)
|
|
|
|
case Kind.membersRemoved(members: []).description:
|
|
self = .membersRemoved(
|
|
members: try container.decode([Data].self, forKey: .members)
|
|
)
|
|
|
|
case Kind.memberLeft.description:
|
|
self = .memberLeft
|
|
|
|
case Kind.encryptionKeyPairRequest.description:
|
|
self = .encryptionKeyPairRequest
|
|
|
|
default: fatalError("Invalid case when trying to decode ClosedGroupControlMessage.Kind")
|
|
}
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(description, forKey: .description)
|
|
|
|
// Note: If you modify the below make sure to update the above 'init(from:)' method
|
|
switch self {
|
|
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
|
|
try container.encode(publicKey, forKey: .publicKey)
|
|
try container.encode(name, forKey: .name)
|
|
try container.encode(encryptionKeyPair.publicKey, forKey: .encryptionPublicKey)
|
|
try container.encode(encryptionKeyPair.secretKey, forKey: .encryptionSecretKey)
|
|
try container.encode(members, forKey: .members)
|
|
try container.encode(admins, forKey: .admins)
|
|
try container.encode(expirationTimer, forKey: .expirationTimer)
|
|
|
|
case .encryptionKeyPair(let publicKey, let wrappers):
|
|
try container.encode(publicKey, forKey: .publicKey)
|
|
try container.encode(wrappers, forKey: .wrappers)
|
|
|
|
case .nameChange(let name):
|
|
try container.encode(name, forKey: .name)
|
|
|
|
case .membersAdded(let members), .membersRemoved(let members):
|
|
try container.encode(members, forKey: .members)
|
|
|
|
case .memberLeft: break // Only 'description'
|
|
case .encryptionKeyPairRequest: break // Only 'description'
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Key Pair Wrapper
|
|
|
|
public struct KeyPairWrapper: Codable {
|
|
public var publicKey: String?
|
|
public var encryptedKeyPair: Data?
|
|
|
|
public var isValid: Bool { publicKey != nil && encryptedKeyPair != nil }
|
|
|
|
public init(publicKey: String, encryptedKeyPair: Data) {
|
|
self.publicKey = publicKey
|
|
self.encryptedKeyPair = encryptedKeyPair
|
|
}
|
|
|
|
// MARK: - Proto Conversion
|
|
|
|
public static func fromProto(_ proto: SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper) -> KeyPairWrapper? {
|
|
return KeyPairWrapper(publicKey: proto.publicKey.toHexString(), encryptedKeyPair: proto.encryptedKeyPair)
|
|
}
|
|
|
|
public func toProto() -> SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper? {
|
|
guard let publicKey = publicKey, let encryptedKeyPair = encryptedKeyPair else { return nil }
|
|
let result = SNProtoDataMessageClosedGroupControlMessageKeyPairWrapper.builder(publicKey: Data(hex: publicKey), encryptedKeyPair: encryptedKeyPair)
|
|
do {
|
|
return try result.build()
|
|
} catch {
|
|
SNLog("Couldn't construct key pair wrapper proto from: \(self).")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Initialization
|
|
|
|
internal init(kind: Kind, sentTimestampMs: UInt64? = nil) {
|
|
super.init(sentTimestamp: sentTimestampMs)
|
|
|
|
self.kind = kind
|
|
}
|
|
|
|
// MARK: - Validation
|
|
|
|
public override var isValid: Bool {
|
|
guard super.isValid, let kind = kind else { return false }
|
|
|
|
switch kind {
|
|
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, _):
|
|
return (
|
|
!publicKey.isEmpty &&
|
|
!name.isEmpty &&
|
|
!encryptionKeyPair.publicKey.isEmpty &&
|
|
!encryptionKeyPair.secretKey.isEmpty &&
|
|
!members.isEmpty &&
|
|
!admins.isEmpty
|
|
)
|
|
|
|
case .encryptionKeyPair: return true
|
|
case .nameChange(let name): return !name.isEmpty
|
|
case .membersAdded(let members): return !members.isEmpty
|
|
case .membersRemoved(let members): return !members.isEmpty
|
|
case .memberLeft: return true
|
|
case .encryptionKeyPairRequest: return true
|
|
}
|
|
}
|
|
|
|
// MARK: - Codable
|
|
|
|
required init(from decoder: Decoder) throws {
|
|
try super.init(from: decoder)
|
|
|
|
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
|
|
|
kind = try container.decode(Kind.self, forKey: .kind)
|
|
}
|
|
|
|
public override func encode(to encoder: Encoder) throws {
|
|
try super.encode(to: encoder)
|
|
|
|
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(kind, forKey: .kind)
|
|
}
|
|
|
|
// MARK: - Proto Conversion
|
|
|
|
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> ClosedGroupControlMessage? {
|
|
guard let closedGroupControlMessageProto = proto.dataMessage?.closedGroupControlMessage else {
|
|
return nil
|
|
}
|
|
|
|
switch closedGroupControlMessageProto.type {
|
|
case .new:
|
|
guard
|
|
let publicKey = closedGroupControlMessageProto.publicKey,
|
|
let name = closedGroupControlMessageProto.name,
|
|
let encryptionKeyPairAsProto = closedGroupControlMessageProto.encryptionKeyPair
|
|
else { return nil }
|
|
|
|
return ClosedGroupControlMessage(
|
|
kind: .new(
|
|
publicKey: publicKey,
|
|
name: name,
|
|
encryptionKeyPair: KeyPair(
|
|
publicKey: encryptionKeyPairAsProto.publicKey.removingIdPrefixIfNeeded().bytes,
|
|
secretKey: encryptionKeyPairAsProto.privateKey.bytes
|
|
),
|
|
members: closedGroupControlMessageProto.members,
|
|
admins: closedGroupControlMessageProto.admins,
|
|
expirationTimer: closedGroupControlMessageProto.expirationTimer
|
|
)
|
|
)
|
|
|
|
case .encryptionKeyPair:
|
|
return ClosedGroupControlMessage(
|
|
kind: .encryptionKeyPair(
|
|
publicKey: closedGroupControlMessageProto.publicKey,
|
|
wrappers: closedGroupControlMessageProto.wrappers
|
|
.compactMap { KeyPairWrapper.fromProto($0) }
|
|
)
|
|
)
|
|
|
|
case .nameChange:
|
|
guard let name = closedGroupControlMessageProto.name else { return nil }
|
|
|
|
return ClosedGroupControlMessage(kind: .nameChange(name: name))
|
|
|
|
case .membersAdded:
|
|
return ClosedGroupControlMessage(
|
|
kind: .membersAdded(members: closedGroupControlMessageProto.members)
|
|
)
|
|
|
|
case .membersRemoved:
|
|
return ClosedGroupControlMessage(
|
|
kind: .membersRemoved(members: closedGroupControlMessageProto.members)
|
|
)
|
|
|
|
case .memberLeft: return ClosedGroupControlMessage(kind: .memberLeft)
|
|
|
|
case .encryptionKeyPairRequest:
|
|
return ClosedGroupControlMessage(kind: .encryptionKeyPairRequest)
|
|
}
|
|
}
|
|
|
|
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
|
|
guard let kind = kind else {
|
|
SNLog("Couldn't construct closed group update proto from: \(self).")
|
|
return nil
|
|
}
|
|
do {
|
|
let closedGroupControlMessage: SNProtoDataMessageClosedGroupControlMessage.SNProtoDataMessageClosedGroupControlMessageBuilder
|
|
switch kind {
|
|
case .new(let publicKey, let name, let encryptionKeyPair, let members, let admins, let expirationTimer):
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .new)
|
|
closedGroupControlMessage.setPublicKey(publicKey)
|
|
closedGroupControlMessage.setName(name)
|
|
let encryptionKeyPairAsProto = SNProtoKeyPair.builder(publicKey: Data(encryptionKeyPair.publicKey), privateKey: Data(encryptionKeyPair.secretKey))
|
|
do {
|
|
closedGroupControlMessage.setEncryptionKeyPair(try encryptionKeyPairAsProto.build())
|
|
} catch {
|
|
SNLog("Couldn't construct closed group update proto from: \(self).")
|
|
return nil
|
|
}
|
|
closedGroupControlMessage.setMembers(members)
|
|
closedGroupControlMessage.setAdmins(admins)
|
|
closedGroupControlMessage.setExpirationTimer(expirationTimer)
|
|
case .encryptionKeyPair(let publicKey, let wrappers):
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .encryptionKeyPair)
|
|
if let publicKey = publicKey {
|
|
closedGroupControlMessage.setPublicKey(publicKey)
|
|
}
|
|
closedGroupControlMessage.setWrappers(wrappers.compactMap { $0.toProto() })
|
|
case .nameChange(let name):
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .nameChange)
|
|
closedGroupControlMessage.setName(name)
|
|
case .membersAdded(let members):
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .membersAdded)
|
|
closedGroupControlMessage.setMembers(members)
|
|
case .membersRemoved(let members):
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .membersRemoved)
|
|
closedGroupControlMessage.setMembers(members)
|
|
case .memberLeft:
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .memberLeft)
|
|
case .encryptionKeyPairRequest:
|
|
closedGroupControlMessage = SNProtoDataMessageClosedGroupControlMessage.builder(type: .encryptionKeyPairRequest)
|
|
}
|
|
let contentProto = SNProtoContent.builder()
|
|
let dataMessageProto = SNProtoDataMessage.builder()
|
|
dataMessageProto.setClosedGroupControlMessage(try closedGroupControlMessage.build())
|
|
|
|
contentProto.setDataMessage(try dataMessageProto.build())
|
|
return try contentProto.build()
|
|
} catch {
|
|
SNLog("Couldn't construct closed group update proto from: \(self).")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Description
|
|
|
|
public var description: String {
|
|
"""
|
|
ClosedGroupControlMessage(
|
|
kind: \(kind?.description ?? "null")
|
|
)
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
public extension ClosedGroupControlMessage.Kind {
|
|
func infoMessage(_ db: Database, sender: String) throws -> String? {
|
|
switch self {
|
|
case .nameChange(let name):
|
|
return "groupNameNew"
|
|
.put(key: "group_name", value: name)
|
|
.localized()
|
|
|
|
case .membersAdded(let membersAsData):
|
|
let memberIds: [String] = membersAsData.map { $0.toHexString() }
|
|
let knownMemberNameMap: [String: String] = try Profile
|
|
.fetchAll(db, ids: memberIds)
|
|
.reduce(into: [:]) { result, next in result[next.id] = next.displayName() }
|
|
let addedMemberNames: [String] = memberIds
|
|
.map {
|
|
knownMemberNameMap[$0] ??
|
|
Profile.truncated(id: $0, threadVariant: .legacyGroup)
|
|
}
|
|
|
|
return "legacyGroupMemberNew"
|
|
.put(key: "name", value: addedMemberNames.joined(separator: ", "))
|
|
.localized()
|
|
|
|
case .membersRemoved(let membersAsData):
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
let memberIds: Set<String> = membersAsData
|
|
.map { $0.toHexString() }
|
|
.asSet()
|
|
|
|
var infoMessage: String = ""
|
|
|
|
if !memberIds.removing(userPublicKey).isEmpty {
|
|
let knownMemberNameMap: [String: String] = try Profile
|
|
.fetchAll(db, ids: memberIds.removing(userPublicKey))
|
|
.reduce(into: [:]) { result, next in result[next.id] = next.displayName() }
|
|
let removedMemberNames: [String] = memberIds.removing(userPublicKey)
|
|
.map {
|
|
knownMemberNameMap[$0] ??
|
|
Profile.truncated(id: $0, threadVariant: .legacyGroup)
|
|
}
|
|
infoMessage = infoMessage.appending(
|
|
"groupRemoved"
|
|
.put(key: "name", value: removedMemberNames.joined(separator: ", "))
|
|
.localized()
|
|
)
|
|
}
|
|
if memberIds.contains(userPublicKey) {
|
|
infoMessage = infoMessage
|
|
.appending(
|
|
"groupRemovedYou"
|
|
.put(key: "group_name", value: "")
|
|
.localized()
|
|
)
|
|
}
|
|
|
|
return infoMessage
|
|
|
|
case .memberLeft:
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
|
|
guard sender != userPublicKey else { return "groupMemberYouLeft".localized() }
|
|
|
|
if let displayName: String = Profile.displayNameNoFallback(db, id: sender) {
|
|
return "groupMemberLeft"
|
|
.put(key: "name", value: displayName)
|
|
.localized()
|
|
}
|
|
|
|
return "groupUpdated".localized()
|
|
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|