feat: admit numbered iPhone companion
This commit is contained in:
parent
fb8607f248
commit
fe1aaade0e
20 changed files with 1563 additions and 7 deletions
|
|
@ -0,0 +1,177 @@
|
|||
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: "")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue