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

247 lines
7.2 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
public protocol OWSDeviceService {
/// Refresh the list of our linked devices.
/// - Returns
/// True if the list changed, and false otherwise.
func refreshDevices() async throws -> Bool
/// Unlink the given device.
func unlinkDevice(deviceId: Int) async throws
/// Renames a device with the given encrypted name.
func renameDevice(
device: OWSDevice,
toEncryptedName encryptedName: String
) async throws
}
extension OWSDeviceService {
public func unlinkDevice(_ device: OWSDevice) async throws {
try await unlinkDevice(deviceId: device.deviceId)
}
}
public enum DeviceRenameError: Error {
case encryptionFailed
case unspecified
}
// MARK: -
struct OWSDeviceServiceImpl: OWSDeviceService {
private let db: any DB
private let deviceNameChangeSyncMessageSender: DeviceNameChangeSyncMessageSender
private let deviceManager: OWSDeviceManager
private let deviceStore: OWSDeviceStore
private let networkManager: NetworkManager
init(
db: any DB,
deviceManager: OWSDeviceManager,
deviceStore: OWSDeviceStore,
messageSenderJobQueue: MessageSenderJobQueue,
networkManager: NetworkManager,
threadStore: ThreadStore
) {
self.db = db
self.deviceNameChangeSyncMessageSender = DeviceNameChangeSyncMessageSender(
messageSenderJobQueue: messageSenderJobQueue,
threadStore: threadStore
)
self.deviceManager = deviceManager
self.deviceStore = deviceStore
self.networkManager = networkManager
}
// MARK: -
func refreshDevices() async throws -> Bool {
let getDevicesResponse = try await networkManager.asyncRequest(
.getDevices(),
canUseWebSocket: true
)
guard let devices = Self.parseDeviceList(response: getDevicesResponse) else {
throw OWSAssertionError("Unable to parse devices response.")
}
let didAddOrRemove = await db.awaitableWrite { tx in
// If we have more than one device we may have a linked device.
// Setting this flag here shouldn't be necessary, but we do so
// because the "cost" is low and it will improve robustness.
if !devices.isEmpty {
deviceManager.setMightHaveUnknownLinkedDevice(
true,
transaction: tx
)
}
return deviceStore.replaceAll(with: devices, tx: tx)
}
return didAddOrRemove
}
private static func parseDeviceList(response: HTTPResponse) -> [OWSDevice]? {
struct DeviceListResponse: Decodable {
struct Device: Decodable {
enum CodingKeys: String, CodingKey {
case createdAtMs = "created"
case lastSeenAtMs = "lastSeen"
case id
case encryptedName = "name"
}
let createdAtMs: UInt64
let lastSeenAtMs: UInt64
let id: Int
let encryptedName: String?
}
let devices: [Device]
}
guard
let devicesJsonData = response.responseBodyData,
let devicesResponse = try? JSONDecoder().decode(DeviceListResponse.self, from: devicesJsonData)
else {
owsFailDebug("Missing or invalid devices response!")
return nil
}
return devicesResponse.devices.compactMap { device in
guard device.id >= OWSDevice.primaryDeviceId else {
owsFailBeta("Invalid device ID: \(device.id)!")
return nil
}
return OWSDevice(
deviceId: device.id,
encryptedName: device.encryptedName,
createdAt: Date(millisecondsSince1970: device.createdAtMs),
lastSeenAt: Date(millisecondsSince1970: device.lastSeenAtMs)
)
}
}
// MARK: -
func unlinkDevice(deviceId: Int) async throws {
_ = try await networkManager.asyncRequest(
.deleteDevice(deviceId: deviceId)
)
}
func renameDevice(
device: OWSDevice,
toEncryptedName encryptedName: String
) async throws {
let response = try await self.networkManager.asyncRequest(
.renameDevice(device: device, encryptedName: encryptedName)
)
guard response.responseStatusCode == 204 else {
throw DeviceRenameError.unspecified
}
await db.awaitableWrite { tx in
deviceStore.setEncryptedName(
encryptedName,
for: device,
tx: tx
)
guard let deviceId = UInt32(exactly: device.deviceId) else {
owsFailDebug("Failed to coerce device ID into UInt32!")
return
}
deviceNameChangeSyncMessageSender.enqueueDeviceNameChangeSyncMessage(
forDeviceId: deviceId,
tx: tx
)
}
}
}
// MARK: -
private struct DeviceNameChangeSyncMessageSender {
private let messageSenderJobQueue: MessageSenderJobQueue
private let threadStore: ThreadStore
init(messageSenderJobQueue: MessageSenderJobQueue, threadStore: ThreadStore) {
self.messageSenderJobQueue = messageSenderJobQueue
self.threadStore = threadStore
}
func enqueueDeviceNameChangeSyncMessage(
forDeviceId deviceId: UInt32,
tx: DBWriteTransaction
) {
let sdsTx = SDSDB.shimOnlyBridge(tx)
guard let localThread = threadStore.getOrCreateLocalThread(tx: tx) else {
owsFailDebug("Failed to create local thread!")
return
}
let outgoingSyncMessage = OutgoingDeviceNameChangeSyncMessage(
deviceId: deviceId,
thread: localThread,
tx: sdsTx
)
messageSenderJobQueue.add(
message: .preprepared(transientMessageWithoutAttachments: outgoingSyncMessage),
transaction: sdsTx
)
}
}
// MARK: -
private extension TSRequest {
static func getDevices() -> TSRequest {
return TSRequest(
url: URL(string: "v1/devices")!,
method: "GET",
parameters: [:]
)
}
static func deleteDevice(
deviceId: Int
) -> TSRequest {
return TSRequest(
url: URL(string: "/v1/devices/\(deviceId)")!,
method: "DELETE",
parameters: nil
)
}
static func renameDevice(
device: OWSDevice,
encryptedName: String
) -> TSRequest {
var urlComponents = URLComponents(string: "v1/accounts/name")!
urlComponents.queryItems = [URLQueryItem(
name: "deviceId",
value: "\(device.deviceId)"
)]
let request = TSRequest(
url: urlComponents.url!,
method: "PUT",
parameters: [
"deviceName": encryptedName,
]
)
request.applyRedactionStrategy(.redactURLForSuccessResponses())
return request
}
}