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.
		
		
		
		
		
			
		
			
				
	
	
		
			433 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			433 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import GRDB
 | |
| import Sodium
 | |
| 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"
 | |
|                 case .encryptionKeyPair: return "encryptionKeyPair"
 | |
|                 case .nameChange: return "nameChange"
 | |
|                 case .membersAdded: return "membersAdded"
 | |
|                 case .membersRemoved: return "membersRemoved"
 | |
|                 case .memberLeft: return "memberLeft"
 | |
|                 case .encryptionKeyPairRequest: return "encryptionKeyPairRequest"
 | |
|             }
 | |
|         }
 | |
|         
 | |
|         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 String(format: "GROUP_TITLE_CHANGED".localized(), name)
 | |
|                 
 | |
|             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 String(
 | |
|                     format: "GROUP_MEMBER_JOINED".localized(),
 | |
|                     addedMemberNames.joined(separator: ", ")
 | |
|                 )
 | |
|                 
 | |
|             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)
 | |
|                         }
 | |
|                     let format: String = (removedMemberNames.count > 1 ?
 | |
|                         "GROUP_MEMBERS_REMOVED".localized() :
 | |
|                         "GROUP_MEMBER_REMOVED".localized()
 | |
|                     )
 | |
| 
 | |
|                     infoMessage = infoMessage.appending(
 | |
|                         String(format: format, removedMemberNames.joined(separator: ", "))
 | |
|                     )
 | |
|                 }
 | |
|                 
 | |
|                 if memberIds.contains(userPublicKey) {
 | |
|                     infoMessage = infoMessage.appending("YOU_WERE_REMOVED".localized())
 | |
|                 }
 | |
|                 
 | |
|                 return infoMessage
 | |
|                 
 | |
|             case .memberLeft:
 | |
|                 let userPublicKey: String = getUserHexEncodedPublicKey(db)
 | |
|                 
 | |
|                 guard sender != userPublicKey else { return "GROUP_YOU_LEFT".localized() }
 | |
|                 
 | |
|                 if let displayName: String = Profile.displayNameNoFallback(db, id: sender) {
 | |
|                     return String(format: "GROUP_MEMBER_LEFT".localized(), displayName)
 | |
|                 }
 | |
|                 
 | |
|                 return "GROUP_UPDATED".localized()
 | |
|                 
 | |
|             default: return nil
 | |
|         }
 | |
|     }
 | |
| }
 |