mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:20:43 +00:00
Keep WhatsApp QR login state synced across gateway, macOS, and UI wait flows. - Preserve the latest QR data URL/version while login polling rotates codes. - Keep the wait-result protocol bounded to current QR metadata. - Stabilize QR rendering and media fixture coverage after rebasing on main. Validation: - pnpm test extensions/whatsapp/src/login-qr.test.ts extensions/whatsapp/src/media.test.ts extensions/whatsapp/src/agent-tools-login.test.ts src/gateway/protocol/channels.schema.test.ts src/gateway/server-methods/web.start.test.ts ui/src/ui/controllers/channels.test.ts - pnpm test:extension whatsapp - cd apps/macos && swift test --filter ChannelsSettingsSmokeTests - GitHub PR checks: 62 success, 5 skipped
194 lines
6.4 KiB
Swift
194 lines
6.4 KiB
Swift
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 }
|
|
guard self.pollTask == nil else { return }
|
|
self.pollTask = Task.detached { [weak self] in
|
|
guard let self else { return }
|
|
await self.refresh(probe: true)
|
|
await self.loadConfigSchema()
|
|
await self.loadConfig()
|
|
while !Task.isCancelled {
|
|
try? await Task.sleep(nanoseconds: UInt64(self.interval * 1_000_000_000))
|
|
await self.refresh(probe: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stop() {
|
|
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 params: [String: AnyCodable] = [
|
|
"probe": AnyCodable(probe),
|
|
"timeoutMs": AnyCodable(8000),
|
|
]
|
|
let snap: ChannelsStatusSnapshot = try await GatewayConnection.shared.requestDecoded(
|
|
method: .channelsStatus,
|
|
params: params,
|
|
timeoutMs: 12000)
|
|
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?
|
|
}
|