Files
sing-box-for-apple/ApplicationLibrary/Views/Tools/TailscaleStatusViewModel.swift
T
2026-04-23 08:12:53 +08:00

179 lines
6.0 KiB
Swift

import Foundation
import Libbox
import Library
import SwiftUI
public struct TailscalePeerData: Identifiable {
public let id: String
public let hostName: String
public let dnsName: String
public let os: String
public let tailscaleIPs: [String]
public let online: Bool
public let exitNode: Bool
public let exitNodeOption: Bool
public let active: Bool
public let rxBytes: Int64
public let txBytes: Int64
public let keyExpiry: Int64
}
public struct TailscaleUserGroupData: Identifiable {
public let id: Int64
public let loginName: String
public let displayName: String
public let profilePicURL: String
public let peers: [TailscalePeerData]
}
public struct TailscaleEndpointData: Identifiable {
public let id: String
public let endpointTag: String
public let backendState: String
public let authURL: String
public let networkName: String
public let magicDNSSuffix: String
public let selfPeer: TailscalePeerData?
public let userGroups: [TailscaleUserGroupData]
}
@MainActor
public final class TailscaleStatusViewModel: BaseViewModel {
@Published public var endpoints: [TailscaleEndpointData] = []
@Published public var isSubscribed = false
private var runningTask: Task<Void, Never>?
public func subscribe() {
guard !isSubscribed else { return }
isSubscribed = true
let handler = StatusHandler(self)
runningTask = Task { [weak self] in
do {
try await Task.detached {
try LibboxNewStandaloneCommandClient()!.subscribeTailscaleStatus(handler)
}.value
} catch {
guard let self else { return }
self.isSubscribed = false
self.endpoints = []
}
self?.runningTask = nil
}
}
public func cancel() {
runningTask?.cancel()
runningTask = nil
isSubscribed = false
endpoints = []
}
public func endpoint(tag: String) -> TailscaleEndpointData? {
endpoints.first { $0.endpointTag == tag }
}
private final class StatusHandler: NSObject, LibboxTailscaleStatusHandlerProtocol, @unchecked Sendable {
private weak var viewModel: TailscaleStatusViewModel?
init(_ viewModel: TailscaleStatusViewModel?) {
self.viewModel = viewModel
}
func onStatusUpdate(_ status: LibboxTailscaleStatusUpdate?) {
guard let status else { return }
let endpoints = Self.convertUpdate(status)
DispatchQueue.main.async { [self] in
guard let viewModel, viewModel.isSubscribed else { return }
viewModel.endpoints = endpoints
}
}
func onError(_ message: String?) {
DispatchQueue.main.async { [self] in
guard let viewModel, viewModel.isSubscribed else { return }
viewModel.isSubscribed = false
viewModel.endpoints = []
if let message {
viewModel.alert = AlertState(errorMessage: message)
}
}
}
private static func convertUpdate(_ status: LibboxTailscaleStatusUpdate) -> [TailscaleEndpointData] {
var endpoints: [TailscaleEndpointData] = []
if let iterator = status.endpoints() {
while iterator.hasNext() {
if let endpoint = iterator.next() {
endpoints.append(convertEndpoint(endpoint))
}
}
}
return endpoints
}
private static func convertEndpoint(_ endpoint: LibboxTailscaleEndpointStatus) -> TailscaleEndpointData {
var userGroups: [TailscaleUserGroupData] = []
if let groupIterator = endpoint.userGroups() {
while groupIterator.hasNext() {
if let group = groupIterator.next() {
userGroups.append(convertUserGroup(group))
}
}
}
return TailscaleEndpointData(
id: endpoint.endpointTag,
endpointTag: endpoint.endpointTag,
backendState: endpoint.backendState,
authURL: endpoint.authURL,
networkName: endpoint.networkName,
magicDNSSuffix: endpoint.magicDNSSuffix,
selfPeer: endpoint.self_ != nil ? convertPeer(endpoint.self_!) : nil,
userGroups: userGroups
)
}
private static func convertUserGroup(_ group: LibboxTailscaleUserGroup) -> TailscaleUserGroupData {
var peers: [TailscalePeerData] = []
if let peerIterator = group.peers() {
while peerIterator.hasNext() {
if let peer = peerIterator.next() {
peers.append(convertPeer(peer))
}
}
}
return TailscaleUserGroupData(
id: group.userID,
loginName: group.loginName,
displayName: group.displayName,
profilePicURL: group.profilePicURL,
peers: peers
)
}
private static func convertPeer(_ peer: LibboxTailscalePeer) -> TailscalePeerData {
var ips: [String] = []
if let ipIterator = peer.tailscaleIPs() {
while ipIterator.hasNext() {
ips.append(ipIterator.next())
}
}
return TailscalePeerData(
id: peer.dnsName.isEmpty ? peer.hostName : peer.dnsName,
hostName: peer.hostName,
dnsName: peer.dnsName,
os: peer.os,
tailscaleIPs: ips,
online: peer.online,
exitNode: peer.exitNode,
exitNodeOption: peer.exitNodeOption,
active: peer.active,
rxBytes: peer.rxBytes,
txBytes: peer.txBytes,
keyExpiry: peer.keyExpiry
)
}
}
}