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

import Foundation
public import GRDB
public import SignalRingRTC

public struct CallLinkRecord: Codable, PersistableRecord, FetchableRecord {
    public static let databaseTableName: String = "CallLink"

    public let id: Int64
    public let roomId: Data
    public let rootKey: CallLinkRootKey
    public var adminPasskey: Data?
    public private(set) var adminDeletedAtTimestampMs: UInt64?
    public var activeCallId: UInt64?
    public private(set) var pendingFetchCounter: Int64
    public private(set) var isUpcoming: Bool?
    public private(set) var name: String?
    public private(set) var restrictions: Restrictions?
    public private(set) var revoked: Bool?
    public private(set) var expiration: Int64?

    init(
        id: Int64,
        roomId: Data,
        rootKey: CallLinkRootKey,
        adminPasskey: Data?,
        adminDeletedAtTimestampMs: UInt64?,
        activeCallId: UInt64?,
        pendingFetchCounter: Int64,
        isUpcoming: Bool?,
        name: String?,
        restrictions: Restrictions?,
        revoked: Bool?,
        expiration: Int64?,
    ) {
        self.id = id
        self.roomId = roomId
        self.rootKey = rootKey
        self.adminPasskey = adminPasskey
        self.adminDeletedAtTimestampMs = adminDeletedAtTimestampMs
        self.activeCallId = activeCallId
        self.pendingFetchCounter = pendingFetchCounter
        self.isUpcoming = isUpcoming
        self.name = name
        self.restrictions = restrictions
        self.revoked = revoked
        self.expiration = expiration
    }

    enum CodingKeys: String, CodingKey {
        case id
        case roomId
        case rootKey
        case adminPasskey
        case adminDeletedAtTimestampMs
        case activeCallId
        case pendingFetchCounter = "pendingActionCounter"
        case isUpcoming
        case name
        case restrictions
        case revoked
        case expiration
    }

    public func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.id, forKey: .id)
        try container.encode(self.roomId, forKey: .roomId)
        try container.encode(self.rootKey.bytes, forKey: .rootKey)
        try container.encodeIfPresent(self.adminPasskey, forKey: .adminPasskey)
        try container.encodeIfPresent(self.adminDeletedAtTimestampMs.map(Int64.init(bitPattern:)), forKey: .adminDeletedAtTimestampMs)
        try container.encodeIfPresent(self.activeCallId.map(Int64.init(bitPattern:)), forKey: .activeCallId)
        try container.encode(self.pendingFetchCounter, forKey: .pendingFetchCounter)
        try container.encodeIfPresent(self.isUpcoming, forKey: .isUpcoming)
        try container.encodeIfPresent(self.name, forKey: .name)
        try container.encodeIfPresent(self.restrictions?.rawValue, forKey: .restrictions)
        try container.encodeIfPresent(self.revoked, forKey: .revoked)
        try container.encodeIfPresent(self.expiration, forKey: .expiration)
    }

    public init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(Int64.self, forKey: .id)
        self.roomId = try container.decode(Data.self, forKey: .roomId)
        self.rootKey = try CallLinkRootKey(container.decode(Data.self, forKey: .rootKey))
        self.adminPasskey = try container.decodeIfPresent(Data.self, forKey: .adminPasskey)
        self.adminDeletedAtTimestampMs = try container.decodeIfPresent(Int64.self, forKey: .adminDeletedAtTimestampMs).map(UInt64.init(bitPattern:))
        self.activeCallId = try container.decodeIfPresent(Int64.self, forKey: .activeCallId).map(UInt64.init(bitPattern:))
        self.pendingFetchCounter = try container.decode(Int64.self, forKey: .pendingFetchCounter)
        self.isUpcoming = try container.decodeIfPresent(Bool.self, forKey: .isUpcoming)
        self.name = try container.decodeIfPresent(String.self, forKey: .name)
        self.restrictions = try container.decodeIfPresent(Int.self, forKey: .restrictions).map { rawValue in
            guard let result = Restrictions(rawValue: rawValue) else {
                throw DecodingError.dataCorruptedError(forKey: .restrictions, in: container, debugDescription: "")
            }
            return result
        }
        self.revoked = try container.decodeIfPresent(Bool.self, forKey: .revoked)
        self.expiration = try container.decodeIfPresent(Int64.self, forKey: .expiration)
    }

    static func insertRecord(rootKey: CallLinkRootKey, tx: DBWriteTransaction) throws -> CallLinkRecord {
        do {
            return try CallLinkRecord.fetchOne(
                tx.database,
                sql: """
                INSERT INTO "CallLink" ("roomId", "rootKey") VALUES (?, ?) RETURNING *
                """,
                arguments: [rootKey.deriveRoomId(), rootKey.bytes],
            )!
        } catch {
            throw error.grdbErrorForLogging
        }
    }

    static func insertFromBackup(
        rootKey: CallLinkRootKey,
        adminPasskey: Data?,
        name: String?,
        restrictions: CallLinkRecord.Restrictions?,
        revoked: Bool?,
        expiration: Int64?,
        isUpcoming: Bool?,
        tx: DBWriteTransaction,
    ) throws -> CallLinkRecord {
        owsPrecondition(isUpcoming != true || adminPasskey != nil)
        do {
            return try CallLinkRecord.fetchOne(
                tx.database,
                sql: """
                INSERT INTO \(CallLinkRecord.databaseTableName) (
                    \(CallLinkRecord.CodingKeys.roomId.rawValue),
                    \(CallLinkRecord.CodingKeys.rootKey.rawValue),
                    \(CallLinkRecord.CodingKeys.adminPasskey.rawValue),
                    \(CallLinkRecord.CodingKeys.name.rawValue),
                    \(CallLinkRecord.CodingKeys.restrictions.rawValue),
                    \(CallLinkRecord.CodingKeys.revoked.rawValue),
                    \(CallLinkRecord.CodingKeys.expiration.rawValue),
                    \(CallLinkRecord.CodingKeys.isUpcoming.rawValue)
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *
                """,
                arguments: [
                    rootKey.deriveRoomId(),
                    rootKey.bytes,
                    adminPasskey,
                    name,
                    restrictions?.rawValue,
                    revoked,
                    expiration,
                    isUpcoming,
                ],
            )!
        } catch {
            throw error.grdbErrorForLogging
        }
    }

    public mutating func clearNeedsFetch() {
        self.pendingFetchCounter = 0
    }

    public mutating func setNeedsFetch() {
        self.pendingFetchCounter += 1
    }

    public enum Restrictions: Int {
        case none = 0
        case adminApproval = 1
        case unknown = -1

        init(_ ringRtcValue: SignalRingRTC.CallLinkState.Restrictions) {
            switch ringRtcValue {
            case .none: self = .none
            case .adminApproval: self = .adminApproval
            case .unknown: self = .unknown
            }
        }

        var asRingRtcValue: SignalRingRTC.CallLinkState.Restrictions {
            switch self {
            case .none: .none
            case .adminApproval: .adminApproval
            case .unknown: .unknown
            }
        }
    }

    public mutating func updateState(_ callLinkState: CallLinkState) {
        self.name = callLinkState.name
        self.restrictions = .some(callLinkState.requiresAdminApproval ? .adminApproval : .none)
        self.revoked = callLinkState.revoked
        self.expiration = Int64(callLinkState.expiration.timeIntervalSince1970)
        self.didUpdateState()
    }

    public var state: CallLinkState? {
        if let restrictions, let revoked, let expiration {
            return CallLinkState(
                name: self.name,
                restrictions: restrictions.asRingRtcValue,
                revoked: revoked,
                expiration: Date(timeIntervalSince1970: TimeInterval(expiration)),
                rootKey: rootKey,
            )
        }
        return nil
    }

    private mutating func didUpdateState() {
        // If we haven't used the link & we're an admin, mark it as upcoming.
        self.isUpcoming = self.isUpcoming ?? (self.adminPasskey != nil)
    }

    mutating func didInsertCallRecord() {
        self.isUpcoming = false
    }

    public var isDeleted: Bool {
        return self.adminDeletedAtTimestampMs != nil
    }

    public mutating func markDeleted(atTimestampMs timestampMs: UInt64) {
        self.adminPasskey = nil
        self.adminDeletedAtTimestampMs = timestampMs
        self.name = nil
        self.restrictions = nil
        self.revoked = nil
        self.expiration = nil
        self.pendingFetchCounter = 0
        self.isUpcoming = false
    }
}