Files
openclaw/apps/ios/Sources/Gateway/GatewayConnectionController+Capabilities.swift
2026-07-13 03:53:15 +02:00

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