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 { "Secure connection is required for this host." } else if effectiveTLS { nil } else { "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) } 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) } 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.hasEventKitAccess(calendarStatus) let remindersStatus = EKEventStore.authorizationStatus(for: .reminder) permissions["reminders"] = Self.hasEventKitAccess(remindersStatus) let motionStatus = CMMotionActivityManager.authorizationStatus() let pedometerStatus = CMPedometer.authorizationStatus() permissions["motion"] = motionStatus == .authorized || pedometerStatus == .authorized let watchStatus = WatchMessagingService.currentStatusSnapshot() permissions["watchSupported"] = watchStatus.supported permissions["watchPaired"] = watchStatus.paired permissions["watchAppInstalled"] = watchStatus.appInstalled permissions["watchReachable"] = watchStatus.reachable 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 hasEventKitAccess(_ status: EKAuthorizationStatus) -> Bool { status == .fullAccess || status == .writeOnly } 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() } 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