TM-SGNL-iOS/SignalServiceKit/Calls/CallLinkRecord.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

231 lines
8.6 KiB
Swift

//
// 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?
private(set) public var adminDeletedAtTimestampMs: UInt64?
public var activeCallId: UInt64?
private(set) public var pendingFetchCounter: Int64
private(set) public var isUpcoming: Bool?
private(set) public var name: String?
private(set) public var restrictions: Restrictions?
private(set) public var revoked: Bool?
private(set) public 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.databaseConnection,
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,
expiration: UInt64,
isUpcoming: Bool,
tx: DBWriteTransaction
) throws -> CallLinkRecord {
do {
return try CallLinkRecord.fetchOne(
tx.databaseConnection,
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.expiration.rawValue),
\(CallLinkRecord.CodingKeys.isUpcoming.rawValue)
) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *
""",
arguments: [
rootKey.deriveRoomId(),
rootKey.bytes,
adminPasskey,
name,
restrictions.rawValue,
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))
)
}
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
}
}