Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Groups/TSGroupModelBuilder.swift
1 views
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import LibSignalClient

public struct TSGroupModelBuilder {

    private enum GroupVersion {
        case V1(groupId: Data)
        case V2(secretParamsData: Data)
    }

    private let groupVersion: GroupVersion

    public var name: String?
    public var descriptionText: String?
    public var avatarDataState: TSGroupModel.AvatarDataState = .missing
    public var groupMembership = GroupMembership()
    public var groupAccess: GroupAccess?
    public var groupV2Revision: UInt32 = 0
    public var avatarUrlPath: String?
    public var inviteLinkPassword: Data?
    public var isAnnouncementsOnly: Bool = false

    public var isJoinRequestPlaceholder: Bool = false
    public var addedByAddress: SignalServiceAddress?
    public var wasJustMigrated: Bool = false
    public var didJustAddSelfViaGroupLink: Bool = false
    public var isTerminated: Bool = false

    private init(groupVersion: GroupVersion) {
        self.groupVersion = groupVersion
    }

    public init(secretParams: GroupSecretParams) {
        self.init(groupVersion: .V2(secretParamsData: secretParams.serialize()))
    }

    fileprivate init(groupModel: TSGroupModel) {
        if let v2 = groupModel as? TSGroupModelV2 {
            self.init(groupVersion: .V2(secretParamsData: v2.secretParamsData))

            self.groupAccess = v2.access
            self.groupV2Revision = v2.revision
            self.avatarUrlPath = v2.avatarUrlPath
            self.inviteLinkPassword = v2.inviteLinkPassword
            self.isAnnouncementsOnly = v2.isAnnouncementsOnly
            self.descriptionText = v2.descriptionText
            self.isTerminated = v2.isTerminated

            // Do not copy transient properties:
            //
            // * isJoinRequestPlaceholder
            // * wasJustMigrated
            // * didJustAddSelfViaGroupLink
            //
            // We want to discard these values when updating group models.
        } else {
            self.init(groupVersion: .V1(groupId: groupModel.groupId))
        }

        self.name = groupModel.groupName
        self.avatarDataState = groupModel.avatarDataState
        self.groupMembership = groupModel.groupMembership
        self.addedByAddress = groupModel.addedByAddress
    }

    // Convert a group state proto received from the service
    // into a group model.
    private init(groupV2Snapshot: GroupV2Snapshot) throws {
        self.init(secretParams: groupV2Snapshot.groupSecretParams)
        self.name = groupV2Snapshot.title
        self.descriptionText = groupV2Snapshot.descriptionText
        self.avatarDataState = groupV2Snapshot.avatarDataState
        self.groupMembership = groupV2Snapshot.groupMembership
        self.groupAccess = groupV2Snapshot.groupAccess
        self.groupV2Revision = groupV2Snapshot.revision
        self.avatarUrlPath = groupV2Snapshot.avatarUrlPath
        self.inviteLinkPassword = groupV2Snapshot.inviteLinkPassword
        self.isAnnouncementsOnly = groupV2Snapshot.isAnnouncementsOnly
        self.isJoinRequestPlaceholder = false
        self.wasJustMigrated = false
        self.didJustAddSelfViaGroupLink = false
        self.isTerminated = groupV2Snapshot.isTerminated
    }

    static func builderForSnapshot(groupV2Snapshot: GroupV2Snapshot, transaction: DBWriteTransaction) throws -> TSGroupModelBuilder {
        return try TSGroupModelBuilder(groupV2Snapshot: groupV2Snapshot)
    }

    public mutating func apply(options: TSGroupModelOptions) {
        if options.contains(.didJustAddSelfViaGroupLink) {
            didJustAddSelfViaGroupLink = true
        }
    }

    public func build() throws -> TSGroupModel {
        let allUsers = groupMembership.allMembersOfAnyKind
        for recipientAddress in allUsers {
            guard recipientAddress.isValid else {
                throw OWSAssertionError("Invalid address.")
            }
        }

        var name: String?
        if let strippedName = self.name?.stripped.nilIfEmpty {
            name = strippedName
        }

        switch groupVersion {
        case .V1(let groupId):
            guard GroupManager.isValidGroupId(groupId, groupsVersion: .V1) else {
                throw OWSAssertionError("Invalid groupId.")
            }
            return TSGroupModel(
                groupId: groupId,
                name: name,
                avatarData: avatarDataState.dataIfPresent,
                members: Array(groupMembership.fullMembers),
                addedBy: addedByAddress,
            )
        case .V2(let secretParamsData):
            let groupSecretParams = try GroupSecretParams(contents: secretParamsData)

            return TSGroupModelV2(
                groupId: try groupSecretParams.getPublicParams().getGroupIdentifier().serialize(),
                name: name,
                descriptionText: descriptionText?.stripped.nilIfEmpty,
                avatarDataState: avatarDataState,
                groupMembership: groupMembership,
                groupAccess: groupAccess ?? .defaultForV2,
                revision: groupV2Revision,
                secretParamsData: groupSecretParams.serialize(),
                avatarUrlPath: avatarUrlPath,
                inviteLinkPassword: inviteLinkPassword,
                isAnnouncementsOnly: isAnnouncementsOnly,
                isJoinRequestPlaceholder: isJoinRequestPlaceholder,
                wasJustMigrated: wasJustMigrated,
                didJustAddSelfViaGroupLink: didJustAddSelfViaGroupLink,
                addedByAddress: addedByAddress,
                isTerminated: isTerminated,
            )
        }
    }

    public func buildAsV2() throws -> TSGroupModelV2 {
        guard let model = try build() as? TSGroupModelV2 else {
            throw OWSAssertionError("[GV1] Should be impossible to create a V1 group!")
        }
        return model
    }
}

// MARK: -

public extension TSGroupModel {
    var asBuilder: TSGroupModelBuilder {
        return TSGroupModelBuilder(groupModel: self)
    }
}

// MARK: -

public struct TSGroupModelOptions: OptionSet {
    public let rawValue: Int

    public static let didJustAddSelfViaGroupLink = TSGroupModelOptions(rawValue: 1 << 0)
    public static let throttle = TSGroupModelOptions(rawValue: 1 << 1)

    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}