import Foundation import OpenClawProtocol func whatsappLoginWaitRequestTimeoutMs( startedAt: Date, timeoutMs: Int, didRunFinalWait: inout Bool, now: Date = Date()) -> Int? { let elapsedMs = Int(now.timeIntervalSince(startedAt) * 1000) let remainingMs = max(timeoutMs - elapsedMs, 0) if remainingMs > 0 { return remainingMs } if didRunFinalWait { return nil } didRunFinalWait = true return 1 } extension ChannelsStore { func start() { guard !self.isPreview else { return } self.startCount += 1 guard self.startCount == 1 else { return } guard self.pollTask == nil else { return } self.pollTask = Task.detached { [weak self] in guard let self else { return } await self.refresh(probe: false) async let schemaLoad: Void = self.loadConfigSchema() async let configLoad: Void = self.loadConfig(force: false) _ = await (schemaLoad, configLoad) while !Task.isCancelled { try? await Task.sleep(nanoseconds: UInt64(self.interval * 1_000_000_000)) await self.refresh(probe: false) } } } func stop() { guard !self.isPreview else { return } guard self.startCount > 0 else { return } self.startCount -= 1 guard self.startCount == 0 else { return } self.pollTask?.cancel() self.pollTask = nil } func refresh(probe: Bool) async { guard !self.isRefreshing else { return } self.isRefreshing = true defer { self.isRefreshing = false } do { let statusTimeoutMs = probe ? 8000 : 2500 let params: [String: AnyCodable] = [ "probe": AnyCodable(probe), "timeoutMs": AnyCodable(statusTimeoutMs), ] let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded( method: .channelsStatus, params: params, timeoutMs: probe ? 12000 : 5000) self.snapshot = snap self.lastSuccess = Date() self.lastError = nil } catch { self.lastError = error.localizedDescription } } func startWhatsAppLogin(force: Bool, autoWait: Bool = true) async { guard !self.whatsappBusy else { return } self.whatsappBusy = true defer { self.whatsappBusy = false } var shouldAutoWait = false do { let params: [String: AnyCodable] = [ "force": AnyCodable(force), "timeoutMs": AnyCodable(30000), ] let result: WhatsAppLoginStartResult = try await GatewayConnection.shared.requestDecoded( method: .webLoginStart, params: params, timeoutMs: 35000) self.whatsappLoginMessage = result.message self.whatsappLoginQrDataUrl = result.qrDataUrl self.whatsappLoginConnected = result.connected shouldAutoWait = autoWait && result.qrDataUrl != nil } catch { self.whatsappLoginMessage = error.localizedDescription self.whatsappLoginQrDataUrl = nil self.whatsappLoginConnected = nil } await self.refresh(probe: true) if shouldAutoWait { Task { await self.waitWhatsAppLogin() } } } func waitWhatsAppLogin(timeoutMs: Int = 120_000) async { guard !self.whatsappBusy else { return } self.whatsappBusy = true defer { self.whatsappBusy = false } let startedAt = Date() var didRunFinalWait = false do { while let remainingMs = whatsappLoginWaitRequestTimeoutMs( startedAt: startedAt, timeoutMs: timeoutMs, didRunFinalWait: &didRunFinalWait) { var params: [String: AnyCodable] = [ "timeoutMs": AnyCodable(remainingMs), ] if let currentQrDataUrl = self.whatsappLoginQrDataUrl { params["currentQrDataUrl"] = AnyCodable(currentQrDataUrl) } let result: WhatsAppLoginWaitResult = try await GatewayConnection.shared.requestDecoded( method: .webLoginWait, params: params, timeoutMs: Double(remainingMs) + 5000) self.applyWhatsAppLoginWaitResult(result) if result.connected || result.qrDataUrl == nil || didRunFinalWait { break } } } catch { self.whatsappLoginMessage = error.localizedDescription } await self.refresh(probe: true) } func logoutWhatsApp() async { guard !self.whatsappBusy else { return } self.whatsappBusy = true defer { self.whatsappBusy = false } do { let params: [String: AnyCodable] = [ "channel": AnyCodable("whatsapp"), ] let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded( method: .channelsLogout, params: params, timeoutMs: 15000) self.whatsappLoginMessage = result.cleared ? "Logged out and cleared credentials." : "No WhatsApp session found." self.whatsappLoginQrDataUrl = nil } catch { self.whatsappLoginMessage = error.localizedDescription } await self.refresh(probe: true) } func logoutTelegram() async { guard !self.telegramBusy else { return } self.telegramBusy = true defer { self.telegramBusy = false } do { let params: [String: AnyCodable] = [ "channel": AnyCodable("telegram"), ] let result: ChannelLogoutResult = try await GatewayConnection.shared.requestDecoded( method: .channelsLogout, params: params, timeoutMs: 15000) if result.envToken == true { self.configStatus = "Telegram token still set via env; config cleared." } else { self.configStatus = result.cleared ? "Telegram token cleared." : "No Telegram token configured." } await self.loadConfig() } catch { self.configStatus = error.localizedDescription } await self.refresh(probe: true) } } private struct WhatsAppLoginStartResult: Codable { let qrDataUrl: String? let message: String let connected: Bool? } struct WhatsAppLoginWaitResult: Codable { let connected: Bool let message: String let qrDataUrl: String? } private struct ChannelLogoutResult: Codable { let channel: String? let accountId: String? let cleared: Bool let envToken: Bool? }