mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-15 17:46:09 +00:00
295 lines
12 KiB
Swift
295 lines
12 KiB
Swift
import AVFoundation
|
|
import Contacts
|
|
import CoreLocation
|
|
import CoreMotion
|
|
import EventKit
|
|
import Foundation
|
|
import OpenClawKit
|
|
import ReplayKit
|
|
import Speech
|
|
import UIKit
|
|
|
|
struct GatewayManualTransportPresentation: Equatable {
|
|
let requiresTLS: Bool
|
|
let effectiveTLS: Bool
|
|
let helperText: String?
|
|
}
|
|
|
|
extension GatewayConnectionController {
|
|
func buildGatewayURL(host: String, port: Int, useTLS: Bool) -> URL? {
|
|
let scheme = useTLS ? "wss" : "ws"
|
|
var components = URLComponents()
|
|
components.scheme = scheme
|
|
components.host = host
|
|
components.port = port
|
|
return components.url
|
|
}
|
|
|
|
func resolveManualUseTLS(host: String, useTLS: Bool) -> Bool {
|
|
Self.manualTransportPresentation(
|
|
host: host,
|
|
requestedTLS: useTLS).effectiveTLS
|
|
}
|
|
|
|
static func manualTransportPresentation(
|
|
host: String,
|
|
requestedTLS: Bool) -> GatewayManualTransportPresentation
|
|
{
|
|
let trimmedHost = host.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let requiresTLS = !trimmedHost.isEmpty && !LoopbackHost.isLocalNetworkHost(trimmedHost)
|
|
let effectiveTLS = requestedTLS || requiresTLS
|
|
let helperText: String? = if requiresTLS {
|
|
String(localized: "Secure connection is required for this host.")
|
|
} else if effectiveTLS {
|
|
nil
|
|
} else {
|
|
String(localized: "Use only on a trusted private network.")
|
|
}
|
|
return GatewayManualTransportPresentation(
|
|
requiresTLS: requiresTLS,
|
|
effectiveTLS: effectiveTLS,
|
|
helperText: helperText)
|
|
}
|
|
|
|
func manualStableID(host: String, port: Int) -> String {
|
|
ManualAuthOverride.manualStableID(host: host, port: port)
|
|
}
|
|
|
|
func makeConnectOptions(
|
|
stableID: String?,
|
|
deviceAuthGatewayID: String?,
|
|
allowStoredDeviceAuth: Bool = true) async -> GatewayConnectOptions
|
|
{
|
|
let defaults = UserDefaults.standard
|
|
let displayName = self.resolvedDisplayName(defaults: defaults)
|
|
let resolvedClientId = self.resolvedClientId(defaults: defaults, stableID: stableID)
|
|
let permissions = await self.currentPermissions()
|
|
|
|
return GatewayConnectOptions(
|
|
role: "node",
|
|
scopes: [],
|
|
caps: self.currentCaps(),
|
|
commands: self.currentCommands(),
|
|
permissions: permissions,
|
|
clientId: resolvedClientId,
|
|
clientMode: "node",
|
|
clientDisplayName: displayName,
|
|
allowStoredDeviceAuth: allowStoredDeviceAuth,
|
|
deviceAuthGatewayID: GatewayStableIdentifier.exact(deviceAuthGatewayID))
|
|
}
|
|
|
|
private func resolvedClientId(defaults: UserDefaults, stableID: String?) -> String {
|
|
if let stableID,
|
|
let override = GatewaySettingsStore.loadGatewayClientIdOverride(stableID: stableID)
|
|
{
|
|
return override
|
|
}
|
|
let manualClientId = defaults.string(forKey: "gateway.manual.clientId")?
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
if manualClientId?.isEmpty == false {
|
|
return manualClientId!
|
|
}
|
|
return "openclaw-ios"
|
|
}
|
|
|
|
private func resolvedDisplayName(defaults: UserDefaults) -> String {
|
|
let key = "node.displayName"
|
|
let existingRaw = defaults.string(forKey: key)
|
|
let resolved = NodeDisplayName.resolve(
|
|
existing: existingRaw,
|
|
deviceName: UIDevice.current.name,
|
|
interfaceIdiom: UIDevice.current.userInterfaceIdiom)
|
|
let existing = existingRaw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
if existing.isEmpty || NodeDisplayName.isGeneric(existing) {
|
|
defaults.set(resolved, forKey: key)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
private func currentCaps() -> [String] {
|
|
var caps = [
|
|
OpenClawCapability.canvas.rawValue,
|
|
OpenClawCapability.screen.rawValue,
|
|
]
|
|
|
|
// Default-on: if the key doesn't exist yet, treat it as enabled.
|
|
let cameraEnabled =
|
|
UserDefaults.standard.object(forKey: "camera.enabled") == nil
|
|
? true
|
|
: UserDefaults.standard.bool(forKey: "camera.enabled")
|
|
if cameraEnabled { caps.append(OpenClawCapability.camera.rawValue) }
|
|
|
|
let voiceWakeEnabled = UserDefaults.standard.bool(forKey: VoiceWakePreferences.enabledKey)
|
|
if voiceWakeEnabled { caps.append(OpenClawCapability.voiceWake.rawValue) }
|
|
|
|
let locationModeRaw = UserDefaults.standard.string(forKey: "location.enabledMode") ?? "off"
|
|
let locationMode = OpenClawLocationMode(rawValue: locationModeRaw) ?? .off
|
|
if locationMode != .off { caps.append(OpenClawCapability.location.rawValue) }
|
|
|
|
caps.append(OpenClawCapability.device.rawValue)
|
|
caps.append(OpenClawCapability.talk.rawValue)
|
|
if WatchMessagingService.isSupportedOnDevice() {
|
|
caps.append(OpenClawCapability.watch.rawValue)
|
|
}
|
|
caps.append(OpenClawCapability.photos.rawValue)
|
|
caps.append(OpenClawCapability.contacts.rawValue)
|
|
caps.append(OpenClawCapability.calendar.rawValue)
|
|
caps.append(OpenClawCapability.reminders.rawValue)
|
|
if Self.motionAvailable() {
|
|
caps.append(OpenClawCapability.motion.rawValue)
|
|
}
|
|
if HealthAuthorization.isEnabled {
|
|
caps.append(OpenClawCapability.health.rawValue)
|
|
}
|
|
|
|
return caps
|
|
}
|
|
|
|
private func currentCommands() -> [String] {
|
|
var commands: [String] = [
|
|
OpenClawCanvasCommand.present.rawValue,
|
|
OpenClawCanvasCommand.hide.rawValue,
|
|
OpenClawCanvasCommand.navigate.rawValue,
|
|
OpenClawCanvasCommand.evalJS.rawValue,
|
|
OpenClawCanvasCommand.snapshot.rawValue,
|
|
OpenClawCanvasA2UICommand.push.rawValue,
|
|
OpenClawCanvasA2UICommand.pushJSONL.rawValue,
|
|
OpenClawCanvasA2UICommand.reset.rawValue,
|
|
OpenClawScreenCommand.record.rawValue,
|
|
OpenClawSystemCommand.notify.rawValue,
|
|
OpenClawChatCommand.push.rawValue,
|
|
OpenClawTalkCommand.pttStart.rawValue,
|
|
OpenClawTalkCommand.pttStop.rawValue,
|
|
OpenClawTalkCommand.pttCancel.rawValue,
|
|
OpenClawTalkCommand.pttOnce.rawValue,
|
|
]
|
|
|
|
let caps = Set(self.currentCaps())
|
|
if caps.contains(OpenClawCapability.camera.rawValue) {
|
|
commands.append(OpenClawCameraCommand.list.rawValue)
|
|
commands.append(OpenClawCameraCommand.snap.rawValue)
|
|
commands.append(OpenClawCameraCommand.clip.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.location.rawValue) {
|
|
commands.append(OpenClawLocationCommand.get.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.device.rawValue) {
|
|
commands.append(OpenClawDeviceCommand.status.rawValue)
|
|
commands.append(OpenClawDeviceCommand.info.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.watch.rawValue) {
|
|
commands.append(OpenClawWatchCommand.status.rawValue)
|
|
commands.append(OpenClawWatchCommand.notify.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.photos.rawValue) {
|
|
commands.append(OpenClawPhotosCommand.latest.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.contacts.rawValue) {
|
|
commands.append(OpenClawContactsCommand.search.rawValue)
|
|
commands.append(OpenClawContactsCommand.add.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.calendar.rawValue) {
|
|
commands.append(OpenClawCalendarCommand.events.rawValue)
|
|
commands.append(OpenClawCalendarCommand.add.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.reminders.rawValue) {
|
|
commands.append(OpenClawRemindersCommand.list.rawValue)
|
|
commands.append(OpenClawRemindersCommand.add.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.motion.rawValue) {
|
|
commands.append(OpenClawMotionCommand.activity.rawValue)
|
|
commands.append(OpenClawMotionCommand.pedometer.rawValue)
|
|
}
|
|
if caps.contains(OpenClawCapability.health.rawValue) {
|
|
commands.append(OpenClawHealthCommand.summary.rawValue)
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
private func currentPermissions() async -> [String: Bool] {
|
|
var permissions: [String: Bool] = [:]
|
|
permissions["camera"] = AVCaptureDevice.authorizationStatus(for: .video) == .authorized
|
|
permissions["microphone"] = AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
|
|
permissions["speechRecognition"] = SFSpeechRecognizer.authorizationStatus() == .authorized
|
|
let locationStatus = CLLocationManager().authorizationStatus
|
|
let locationServicesEnabled = await Self.locationServicesEnabled()
|
|
permissions["location"] = Self.isLocationAvailable(
|
|
servicesEnabled: locationServicesEnabled,
|
|
status: locationStatus)
|
|
permissions["screenRecording"] = RPScreenRecorder.shared().isAvailable
|
|
|
|
permissions["photos"] = PhotoLibraryAccess.canRead(PhotoLibraryAccess.authorizationStatus())
|
|
let contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
|
|
permissions["contacts"] = contactsStatus == .authorized || contactsStatus == .limited
|
|
|
|
let calendarStatus = EKEventStore.authorizationStatus(for: .event)
|
|
permissions["calendar"] = Self.hasEventKitReadAccess(calendarStatus)
|
|
let remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
|
|
permissions["reminders"] = Self.hasEventKitReadAccess(remindersStatus)
|
|
|
|
let motionStatus = CMMotionActivityManager.authorizationStatus()
|
|
let pedometerStatus = CMPedometer.authorizationStatus()
|
|
permissions["motion"] =
|
|
motionStatus == .authorized || pedometerStatus == .authorized
|
|
|
|
return permissions
|
|
}
|
|
|
|
private static func locationServicesEnabled() async -> Bool {
|
|
await Task.detached(priority: .utility) {
|
|
CLLocationManager.locationServicesEnabled()
|
|
}.value
|
|
}
|
|
|
|
private static func isLocationAvailable(servicesEnabled: Bool, status: CLAuthorizationStatus) -> Bool {
|
|
guard servicesEnabled else { return false }
|
|
switch status {
|
|
case .authorizedAlways, .authorizedWhenInUse:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
private static func hasEventKitReadAccess(_ status: EKAuthorizationStatus) -> Bool {
|
|
status == .fullAccess
|
|
}
|
|
|
|
private static func motionAvailable() -> Bool {
|
|
CMMotionActivityManager.isActivityAvailable() || CMPedometer.isStepCountingAvailable()
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
extension GatewayConnectionController {
|
|
func _test_resolvedDisplayName(defaults: UserDefaults) -> String {
|
|
self.resolvedDisplayName(defaults: defaults)
|
|
}
|
|
|
|
func _test_currentCaps() -> [String] {
|
|
self.currentCaps()
|
|
}
|
|
|
|
func _test_currentCommands() -> [String] {
|
|
self.currentCommands()
|
|
}
|
|
|
|
func _test_currentPermissions() async -> [String: Bool] {
|
|
await self.currentPermissions()
|
|
}
|
|
|
|
static func _test_hasEventKitReadAccess(_ status: EKAuthorizationStatus) -> Bool {
|
|
self.hasEventKitReadAccess(status)
|
|
}
|
|
|
|
static func _test_isLocationAvailable(servicesEnabled: Bool, status: CLAuthorizationStatus) -> Bool {
|
|
self.isLocationAvailable(servicesEnabled: servicesEnabled, status: status)
|
|
}
|
|
|
|
func _test_resolveManualUseTLS(host: String, useTLS: Bool) -> Bool {
|
|
self.resolveManualUseTLS(host: host, useTLS: useTLS)
|
|
}
|
|
}
|
|
#endif
|