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

public import GRDB
public import LibSignalClient

/// Represents a full member of a group.
///
/// Importantly, this means that invited and requesting group members are
/// **not** represented by a ``TSGroupMember``. See the notes below for more
/// details.
///
/// - Note
/// A ``TSGroupMember`` stores both a serviceId and phone number. Full members
/// of a V2 group can only be represented by their ACI - so for V2 group
/// members the serviceId can be expected to be an ACI. However, phone
/// number-only members of legacy V1 groups may end up with a PNI; for example,
/// that phone-number-only member may become re-registered, and we may
/// subsequently learn about their PNI.
///
/// - Note
/// At the time of writing there exists a `UNIQUE INDEX` on the phone number and
/// group thread ID columns of this model. This is currently safe, as it's
/// impossible for a single phone number (a single account) to be in a group as
/// two different full members. However, it **is** possible for the same account
/// to be both an invited member (by their PNI) and a full member (by their
/// ACI). Take care if this model is ever extended to include invited members.
public final class TSGroupMember: NSObject, SDSCodableModel, Decodable {
    public static let databaseTableName = "model_TSGroupMember"
    private static var recordType: SDSRecordType { .groupMember }

    public enum CodingKeys: String, CodingKey, ColumnExpression {
        case id
        case recordType
        case uniqueId
        case groupThreadId
        case phoneNumber
        case serviceId = "uuidString"
        case lastInteractionTimestamp
    }

    public var id: Int64?
    public let uniqueId: String
    public let serviceId: ServiceId?
    public let phoneNumber: String?
    public let groupThreadId: String
    public private(set) var lastInteractionTimestamp: UInt64

    public init(
        address: NormalizedDatabaseRecordAddress,
        groupThreadId: String,
        lastInteractionTimestamp: UInt64,
    ) {
        self.uniqueId = UUID().uuidString
        self.serviceId = address.serviceId
        self.phoneNumber = address.phoneNumber
        self.groupThreadId = groupThreadId
        self.lastInteractionTimestamp = lastInteractionTimestamp
    }

    // MARK: - Codable

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        let decodedRecordType = try container.decode(Int64.self, forKey: .recordType)
        owsAssertDebug(decodedRecordType == Self.recordType.rawValue, "Unexpectedly decoded record with wrong type.")

        id = try container.decodeIfPresent(RowId.self, forKey: .id)
        uniqueId = try container.decode(String.self, forKey: .uniqueId)
        groupThreadId = try container.decode(String.self, forKey: .groupThreadId)
        serviceId = try container.decodeIfPresent(String.self, forKey: .serviceId)
            .flatMap { try? ServiceId.parseFrom(serviceIdString: $0) }
        phoneNumber = try container.decodeIfPresent(String.self, forKey: .phoneNumber)
        lastInteractionTimestamp = try container.decode(UInt64.self, forKey: .lastInteractionTimestamp)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(id, forKey: .id)
        try container.encode(Self.recordType.rawValue, forKey: .recordType)
        try container.encode(uniqueId, forKey: .uniqueId)
        try container.encode(groupThreadId, forKey: .groupThreadId)
        try container.encodeIfPresent(serviceId?.serviceIdUppercaseString, forKey: .serviceId)
        try container.encodeIfPresent(phoneNumber, forKey: .phoneNumber)
        try container.encode(lastInteractionTimestamp, forKey: .lastInteractionTimestamp)
    }

    // MARK: -

    public func anyUpdateWith(
        lastInteractionTimestamp: UInt64,
        transaction: DBWriteTransaction,
    ) {
        anyUpdate(transaction: transaction) { groupMember in
            groupMember.lastInteractionTimestamp = lastInteractionTimestamp
        }
    }

    public func updateWith(
        lastInteractionTimestamp: UInt64,
        tx: DBWriteTransaction,
    ) throws {
        self.lastInteractionTimestamp = lastInteractionTimestamp
        try self.update(tx.database)
    }

    public class func groupMember(
        for address: SignalServiceAddress,
        in groupThreadId: String,
        transaction: DBReadTransaction,
    ) -> TSGroupMember? {
        let sql = """
            SELECT * FROM \(databaseTableName)
            WHERE (\(columnName(.serviceId)) = ? OR \(columnName(.serviceId)) IS NULL)
            AND (\(columnName(.phoneNumber)) = ? OR \(columnName(.phoneNumber)) IS NULL)
            AND NOT (\(columnName(.serviceId)) IS NULL AND \(columnName(.phoneNumber)) IS NULL)
            AND \(columnName(.groupThreadId)) = ?
            LIMIT 1
        """

        return failIfThrows {
            return try fetchOne(
                transaction.database,
                sql: sql,
                arguments: [address.serviceIdUppercaseString, address.phoneNumber, groupThreadId],
            )
        }
    }

    public class func groupMember(
        for aci: Aci,
        in groupThread: TSGroupThread,
        tx: DBReadTransaction,
    ) throws -> TSGroupMember? {
        return try TSGroupMember
            .filter(Column(CodingKeys.serviceId) == aci.serviceIdUppercaseString)
            .filter(Column(CodingKeys.groupThreadId) == groupThread.uniqueId)
            .fetchOne(tx.database)
    }
}

// MARK: -

public extension TSGroupThread {
    class func groupThreads(
        with address: SignalServiceAddress,
        transaction: DBReadTransaction,
    ) -> [TSGroupThread] {
        let sql = """
            SELECT \(TSGroupMember.columnName(.groupThreadId)) FROM \(TSGroupMember.databaseTableName)
            WHERE (\(TSGroupMember.columnName(.serviceId)) = ? OR \(TSGroupMember.columnName(.serviceId)) IS NULL)
            AND (\(TSGroupMember.columnName(.phoneNumber)) = ? OR \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            AND NOT (\(TSGroupMember.columnName(.serviceId)) IS NULL AND \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC
        """

        return failIfThrows {
            var groupThreads = [TSGroupThread]()

            let cursor = try String.fetchCursor(
                transaction.database,
                sql: sql,
                arguments: [address.serviceIdUppercaseString, address.phoneNumber],
            )

            while let groupThreadId = try cursor.next() {
                guard
                    let groupThread = TSGroupThread.fetchGroupThreadViaCache(
                        uniqueId: groupThreadId,
                        transaction: transaction,
                    )
                else {
                    owsFailDebug("Missing group thread")
                    continue
                }

                groupThreads.append(groupThread)
            }

            return groupThreads
        }
    }

    class func enumerateGroupThreads(
        with address: SignalServiceAddress,
        transaction: DBReadTransaction,
        block: (TSGroupThread, inout Bool) -> Void,
    ) {
        let sql = """
            SELECT \(TSGroupMember.columnName(.groupThreadId)) FROM \(TSGroupMember.databaseTableName)
            WHERE (\(TSGroupMember.columnName(.serviceId)) = ? OR \(TSGroupMember.columnName(.serviceId)) IS NULL)
            AND (\(TSGroupMember.columnName(.phoneNumber)) = ? OR \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            AND NOT (\(TSGroupMember.columnName(.serviceId)) IS NULL AND \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC
        """

        let cursor = try! String.fetchCursor(
            transaction.database,
            sql: sql,
            arguments: [address.serviceIdUppercaseString, address.phoneNumber],
        )

        while let groupThreadId = try! cursor.next() {
            guard
                let groupThread = TSGroupThread.fetchGroupThreadViaCache(
                    uniqueId: groupThreadId,
                    transaction: transaction,
                )
            else {
                owsFailDebug("Missing group thread")
                continue
            }
            var stop = false
            block(groupThread, &stop)
            if stop { return }
        }
    }

    class func groupThreadIds(
        with address: SignalServiceAddress,
        transaction tx: DBReadTransaction,
    ) -> [String] {
        let sql = """
            SELECT \(TSGroupMember.columnName(.groupThreadId))
            FROM \(TSGroupMember.databaseTableName)
            WHERE (\(TSGroupMember.columnName(.serviceId)) = ? OR \(TSGroupMember.columnName(.serviceId)) IS NULL)
            AND (\(TSGroupMember.columnName(.phoneNumber)) = ? OR \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            AND NOT (\(TSGroupMember.columnName(.serviceId)) IS NULL AND \(TSGroupMember.columnName(.phoneNumber)) IS NULL)
            ORDER BY \(TSGroupMember.columnName(.lastInteractionTimestamp)) DESC
        """
        return failIfThrows {
            try String.fetchAll(
                tx.database,
                sql: sql,
                arguments: [address.serviceIdUppercaseString, address.phoneNumber],
            )
        }
    }
}