hololake-system-architecture/product-source/hololake-native-desktop/mobile/ios/Sources/HoloLakeModels.swift

177 lines
4.9 KiB
Swift

import Foundation
import Security
struct PairingDescriptor: Equatable {
let host: String
let port: Int
let pairingID: String
let secret: Data
init(url: URL) throws {
guard url.scheme == "hololake", url.host == "pair",
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
throw HoloLakeMobileError.invalidPairingLink
}
var values: [String: String] = [:]
for item in components.queryItems ?? [] {
guard let value = item.value, values[item.name] == nil else {
throw HoloLakeMobileError.invalidPairingLink
}
values[item.name] = value
}
guard let host = values["host"], !host.isEmpty,
let portText = values["port"], let port = Int(portText), (1...65535).contains(port),
let pairingID = values["id"], UUID(uuidString: pairingID) != nil,
let secretText = values["secret"],
let secret = Data(base64URL: secretText), secret.count == 32 else {
throw HoloLakeMobileError.invalidPairingLink
}
self.host = host
self.port = port
self.pairingID = pairingID
self.secret = secret
}
var baseURL: URL {
URL(string: "http://\(host):\(port)")!
}
}
struct StoredSession: Codable, Equatable {
let host: String
let port: Int
let deviceID: String
let sessionKey: Data
let desktopName: String
var counter: UInt64
var cursor: UInt64
var baseURL: URL {
URL(string: "http://\(host):\(port)")!
}
}
struct PairRequest: Codable {
let deviceName: String
let platform: String
let requestID: String
}
struct PairResponse: Codable {
let schema: String
let state: String
let deviceID: String
let sessionKey: String
let desktopName: String
let rootNodeRole: String
let requestID: String
}
struct SyncRequest: Codable {
let counter: UInt64
let afterCursor: UInt64?
let capture: MobileCaptureInput?
}
struct MobileCaptureInput: Codable {
let title: String
let body: String
let requestID: String
}
struct MobileSyncSnapshot: Codable {
let schema: String
let state: String
let cursor: UInt64
let generatedAtUnixMs: UInt64
let desktopName: String
let rootNodeOnline: Bool
let personalChannel: MobileChannelProjection
let webNovel: MobileWebNovelProjection
let education: MobileEducationProjection
let recentCaptures: [MobileCaptureProjection]
let boundary: MobileBoundaryProjection
}
struct MobileChannelProjection: Codable {
let home: String
let growthEventCount: UInt64
let integrity: String
}
struct MobileWebNovelProjection: Codable {
let workCount: UInt64
let volumeCount: UInt64
let chapterCount: UInt64
let works: [MobileWebNovelWork]
}
struct MobileWebNovelWork: Codable, Identifiable {
let workID: String
let title: String
let status: String
let updatedAtUnixMs: UInt64
var id: String { workID }
}
struct MobileEducationProjection: Codable {
let activeTableCount: UInt64
let archivedTableCount: UInt64
let unassignedTableCount: UInt64
let sensitiveValuesIncluded: Bool
}
struct MobileCaptureProjection: Codable, Identifiable {
let cursor: UInt64
let captureID: String
let title: String
let body: String
let sourceDeviceID: String
let createdAtUnixMs: UInt64
var id: String { captureID }
}
struct MobileBoundaryProjection: Codable {
let mobileRole: String
let remoteDesktopClone: Bool
let desktopOfflineExecution: Bool
let sensitiveEducationValues: String
let modelAPI: String
}
enum HoloLakeMobileError: LocalizedError {
case invalidPairingLink
case invalidResponse
case rejected(String)
case missingNonce
case missingSession
case keychain(OSStatus)
var errorDescription: String? {
switch self {
case .invalidPairingLink: return "配对链接无效或已损坏。"
case .invalidResponse: return "电脑端返回了无法校验的响应。"
case .rejected(let code): return "电脑端拒绝了本次请求:\(code)"
case .missingNonce: return "加密响应缺少一次性随机数。"
case .missingSession: return "请先与电脑端 HoloLake 配对。"
case .keychain(let status): return "本机钥匙串写入失败(\(status))。"
}
}
}
extension Data {
init?(base64URL value: String) {
var text = value.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
text += String(repeating: "=", count: (4 - text.count % 4) % 4)
guard let data = Data(base64Encoded: text) else { return nil }
self = data
}
var base64URL: String {
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}