mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
iOS: add write commands for contacts/calendar/reminders
This commit is contained in:
committed by
Mariano Belinky
parent
f72ac60b01
commit
a884955cd6
@@ -36,6 +36,65 @@ final class CalendarService: CalendarServicing {
|
||||
return OpenClawCalendarEventsPayload(events: payload)
|
||||
}
|
||||
|
||||
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .event)
|
||||
let authorized = await Self.ensureWriteAuthorization(store: store, status: status)
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Calendar", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_PERMISSION_REQUIRED: grant Calendar permission",
|
||||
])
|
||||
}
|
||||
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty else {
|
||||
throw NSError(domain: "Calendar", code: 3, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_INVALID: title required",
|
||||
])
|
||||
}
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
guard let start = formatter.date(from: params.startISO) else {
|
||||
throw NSError(domain: "Calendar", code: 4, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_INVALID: startISO required",
|
||||
])
|
||||
}
|
||||
guard let end = formatter.date(from: params.endISO) else {
|
||||
throw NSError(domain: "Calendar", code: 5, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_INVALID: endISO required",
|
||||
])
|
||||
}
|
||||
|
||||
let event = EKEvent(eventStore: store)
|
||||
event.title = title
|
||||
event.startDate = start
|
||||
event.endDate = end
|
||||
event.isAllDay = params.isAllDay ?? false
|
||||
if let location = params.location?.trimmingCharacters(in: .whitespacesAndNewlines), !location.isEmpty {
|
||||
event.location = location
|
||||
}
|
||||
if let notes = params.notes?.trimmingCharacters(in: .whitespacesAndNewlines), !notes.isEmpty {
|
||||
event.notes = notes
|
||||
}
|
||||
event.calendar = try Self.resolveCalendar(
|
||||
store: store,
|
||||
calendarId: params.calendarId,
|
||||
calendarTitle: params.calendarTitle)
|
||||
|
||||
try store.save(event, span: .thisEvent)
|
||||
|
||||
let payload = OpenClawCalendarEventPayload(
|
||||
identifier: event.eventIdentifier ?? UUID().uuidString,
|
||||
title: event.title ?? title,
|
||||
startISO: formatter.string(from: event.startDate),
|
||||
endISO: formatter.string(from: event.endDate),
|
||||
isAllDay: event.isAllDay,
|
||||
location: event.location,
|
||||
calendarTitle: event.calendar.title)
|
||||
|
||||
return OpenClawCalendarAddPayload(event: payload)
|
||||
}
|
||||
|
||||
private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized:
|
||||
@@ -57,6 +116,54 @@ final class CalendarService: CalendarServicing {
|
||||
}
|
||||
}
|
||||
|
||||
private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized, .fullAccess, .writeOnly:
|
||||
return true
|
||||
case .notDetermined:
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestAccess(to: .event) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
case .restricted, .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func resolveCalendar(
|
||||
store: EKEventStore,
|
||||
calendarId: String?,
|
||||
calendarTitle: String?) throws -> EKCalendar
|
||||
{
|
||||
if let id = calendarId?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty,
|
||||
let calendar = store.calendar(withIdentifier: id)
|
||||
{
|
||||
return calendar
|
||||
}
|
||||
|
||||
if let title = calendarTitle?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
|
||||
if let calendar = store.calendars(for: .event).first(where: {
|
||||
$0.title.compare(title, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
|
||||
}) {
|
||||
return calendar
|
||||
}
|
||||
throw NSError(domain: "Calendar", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_NOT_FOUND: no calendar named \(title)",
|
||||
])
|
||||
}
|
||||
|
||||
if let fallback = store.defaultCalendarForNewEvents {
|
||||
return fallback
|
||||
}
|
||||
|
||||
throw NSError(domain: "Calendar", code: 7, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CALENDAR_NOT_FOUND: no default calendar",
|
||||
])
|
||||
}
|
||||
|
||||
private static func resolveRange(startISO: String?, endISO: String?) -> (Date, Date) {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
let start = startISO.flatMap { formatter.date(from: $0) } ?? Date()
|
||||
|
||||
@@ -3,6 +3,17 @@ import Foundation
|
||||
import OpenClawKit
|
||||
|
||||
final class ContactsService: ContactsServicing {
|
||||
private static var payloadKeys: [CNKeyDescriptor] {
|
||||
[
|
||||
CNContactIdentifierKey as CNKeyDescriptor,
|
||||
CNContactGivenNameKey as CNKeyDescriptor,
|
||||
CNContactFamilyNameKey as CNKeyDescriptor,
|
||||
CNContactOrganizationNameKey as CNKeyDescriptor,
|
||||
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
||||
CNContactEmailAddressesKey as CNKeyDescriptor,
|
||||
]
|
||||
}
|
||||
|
||||
func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload {
|
||||
let store = CNContactStore()
|
||||
let status = CNContactStore.authorizationStatus(for: .contacts)
|
||||
@@ -14,21 +25,13 @@ final class ContactsService: ContactsServicing {
|
||||
}
|
||||
|
||||
let limit = max(1, min(params.limit ?? 25, 200))
|
||||
let keys: [CNKeyDescriptor] = [
|
||||
CNContactIdentifierKey as CNKeyDescriptor,
|
||||
CNContactGivenNameKey as CNKeyDescriptor,
|
||||
CNContactFamilyNameKey as CNKeyDescriptor,
|
||||
CNContactOrganizationNameKey as CNKeyDescriptor,
|
||||
CNContactPhoneNumbersKey as CNKeyDescriptor,
|
||||
CNContactEmailAddressesKey as CNKeyDescriptor,
|
||||
]
|
||||
|
||||
var contacts: [CNContact] = []
|
||||
if let query = params.query?.trimmingCharacters(in: .whitespacesAndNewlines), !query.isEmpty {
|
||||
let predicate = CNContact.predicateForContacts(matchingName: query)
|
||||
contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
|
||||
contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
|
||||
} else {
|
||||
let request = CNContactFetchRequest(keysToFetch: keys)
|
||||
let request = CNContactFetchRequest(keysToFetch: Self.payloadKeys)
|
||||
try store.enumerateContacts(with: request) { contact, stop in
|
||||
contacts.append(contact)
|
||||
if contacts.count >= limit {
|
||||
@@ -38,21 +41,77 @@ final class ContactsService: ContactsServicing {
|
||||
}
|
||||
|
||||
let sliced = Array(contacts.prefix(limit))
|
||||
let payload = sliced.map { contact in
|
||||
OpenClawContactPayload(
|
||||
identifier: contact.identifier,
|
||||
displayName: CNContactFormatter.string(from: contact, style: .fullName)
|
||||
?? "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
givenName: contact.givenName,
|
||||
familyName: contact.familyName,
|
||||
organizationName: contact.organizationName,
|
||||
phoneNumbers: contact.phoneNumbers.map { $0.value.stringValue },
|
||||
emails: contact.emailAddresses.map { String($0.value) })
|
||||
}
|
||||
let payload = sliced.map { Self.payload(from: $0) }
|
||||
|
||||
return OpenClawContactsSearchPayload(contacts: payload)
|
||||
}
|
||||
|
||||
func add(params: OpenClawContactsAddParams) async throws -> OpenClawContactsAddPayload {
|
||||
let store = CNContactStore()
|
||||
let status = CNContactStore.authorizationStatus(for: .contacts)
|
||||
let authorized = await Self.ensureAuthorization(store: store, status: status)
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Contacts", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CONTACTS_PERMISSION_REQUIRED: grant Contacts permission",
|
||||
])
|
||||
}
|
||||
|
||||
let givenName = params.givenName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let familyName = params.familyName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let organizationName = params.organizationName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let displayName = params.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let phoneNumbers = Self.normalizeStrings(params.phoneNumbers)
|
||||
let emails = Self.normalizeStrings(params.emails, lowercased: true)
|
||||
|
||||
let hasName = !(givenName ?? "").isEmpty || !(familyName ?? "").isEmpty || !(displayName ?? "").isEmpty
|
||||
let hasOrg = !(organizationName ?? "").isEmpty
|
||||
let hasDetails = !phoneNumbers.isEmpty || !emails.isEmpty
|
||||
guard hasName || hasOrg || hasDetails else {
|
||||
throw NSError(domain: "Contacts", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "CONTACTS_INVALID: include a name, organization, phone, or email",
|
||||
])
|
||||
}
|
||||
|
||||
if !phoneNumbers.isEmpty || !emails.isEmpty {
|
||||
if let existing = try Self.findExistingContact(
|
||||
store: store,
|
||||
phoneNumbers: phoneNumbers,
|
||||
emails: emails)
|
||||
{
|
||||
return OpenClawContactsAddPayload(contact: Self.payload(from: existing))
|
||||
}
|
||||
}
|
||||
|
||||
let contact = CNMutableContact()
|
||||
contact.givenName = givenName ?? ""
|
||||
contact.familyName = familyName ?? ""
|
||||
contact.organizationName = organizationName ?? ""
|
||||
if contact.givenName.isEmpty && contact.familyName.isEmpty, let displayName {
|
||||
contact.givenName = displayName
|
||||
}
|
||||
contact.phoneNumbers = phoneNumbers.map {
|
||||
CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: $0))
|
||||
}
|
||||
contact.emailAddresses = emails.map {
|
||||
CNLabeledValue(label: CNLabelHome, value: $0 as NSString)
|
||||
}
|
||||
|
||||
let save = CNSaveRequest()
|
||||
save.add(contact, toContainerWithIdentifier: nil)
|
||||
try store.execute(save)
|
||||
|
||||
let persisted: CNContact
|
||||
if !contact.identifier.isEmpty {
|
||||
persisted = try store.unifiedContact(
|
||||
withIdentifier: contact.identifier,
|
||||
keysToFetch: Self.payloadKeys)
|
||||
} else {
|
||||
persisted = contact
|
||||
}
|
||||
|
||||
return OpenClawContactsAddPayload(contact: Self.payload(from: persisted))
|
||||
}
|
||||
|
||||
private static func ensureAuthorization(store: CNContactStore, status: CNAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized, .limited:
|
||||
@@ -69,4 +128,87 @@ final class ContactsService: ContactsServicing {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalizeStrings(_ values: [String]?, lowercased: Bool = false) -> [String] {
|
||||
(values ?? [])
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.map { lowercased ? $0.lowercased() : $0 }
|
||||
}
|
||||
|
||||
private static func findExistingContact(
|
||||
store: CNContactStore,
|
||||
phoneNumbers: [String],
|
||||
emails: [String]) throws -> CNContact?
|
||||
{
|
||||
if phoneNumbers.isEmpty && emails.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var matches: [CNContact] = []
|
||||
|
||||
for phone in phoneNumbers {
|
||||
let predicate = CNContact.predicateForContacts(matching: CNPhoneNumber(stringValue: phone))
|
||||
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
|
||||
matches.append(contentsOf: contacts)
|
||||
}
|
||||
|
||||
for email in emails {
|
||||
let predicate = CNContact.predicateForContacts(matchingEmailAddress: email)
|
||||
let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
|
||||
matches.append(contentsOf: contacts)
|
||||
}
|
||||
|
||||
return Self.matchContacts(contacts: matches, phoneNumbers: phoneNumbers, emails: emails)
|
||||
}
|
||||
|
||||
private static func matchContacts(
|
||||
contacts: [CNContact],
|
||||
phoneNumbers: [String],
|
||||
emails: [String]) -> CNContact?
|
||||
{
|
||||
let normalizedPhones = Set(phoneNumbers.map { normalizePhone($0) }.filter { !$0.isEmpty })
|
||||
let normalizedEmails = Set(emails.map { $0.lowercased() }.filter { !$0.isEmpty })
|
||||
var seen = Set<String>()
|
||||
|
||||
for contact in contacts {
|
||||
guard seen.insert(contact.identifier).inserted else { continue }
|
||||
let contactPhones = Set(contact.phoneNumbers.map { normalizePhone($0.value.stringValue) })
|
||||
let contactEmails = Set(contact.emailAddresses.map { String($0.value).lowercased() })
|
||||
|
||||
if !normalizedPhones.isEmpty, !contactPhones.isDisjoint(with: normalizedPhones) {
|
||||
return contact
|
||||
}
|
||||
if !normalizedEmails.isEmpty, !contactEmails.isDisjoint(with: normalizedEmails) {
|
||||
return contact
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func normalizePhone(_ phone: String) -> String {
|
||||
let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let digits = trimmed.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0) }
|
||||
let normalized = String(String.UnicodeScalarView(digits))
|
||||
return normalized.isEmpty ? trimmed : normalized
|
||||
}
|
||||
|
||||
private static func payload(from contact: CNContact) -> OpenClawContactPayload {
|
||||
OpenClawContactPayload(
|
||||
identifier: contact.identifier,
|
||||
displayName: CNContactFormatter.string(from: contact, style: .fullName)
|
||||
?? "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
givenName: contact.givenName,
|
||||
familyName: contact.familyName,
|
||||
organizationName: contact.organizationName,
|
||||
phoneNumbers: contact.phoneNumbers.map { $0.value.stringValue },
|
||||
emails: contact.emailAddresses.map { String($0.value) })
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
static func _test_matches(contact: CNContact, phoneNumbers: [String], emails: [String]) -> Bool {
|
||||
matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -373,12 +373,15 @@ final class GatewayConnectionController {
|
||||
}
|
||||
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)
|
||||
@@ -400,12 +403,15 @@ final class GatewayConnectionController {
|
||||
|
||||
let photoStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
||||
permissions["photos"] = photoStatus == .authorized || photoStatus == .limited
|
||||
permissions["contacts"] = CNContactStore.authorizationStatus(for: .contacts) == .authorized
|
||||
let contactsStatus = CNContactStore.authorizationStatus(for: .contacts)
|
||||
permissions["contacts"] = contactsStatus == .authorized || contactsStatus == .limited
|
||||
|
||||
let calendarStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
permissions["calendar"] = calendarStatus == .authorized || calendarStatus == .fullAccess
|
||||
permissions["calendar"] =
|
||||
calendarStatus == .authorized || calendarStatus == .fullAccess || calendarStatus == .writeOnly
|
||||
let remindersStatus = EKEventStore.authorizationStatus(for: .reminder)
|
||||
permissions["reminders"] = remindersStatus == .authorized || remindersStatus == .fullAccess
|
||||
permissions["reminders"] =
|
||||
remindersStatus == .authorized || remindersStatus == .fullAccess || remindersStatus == .writeOnly
|
||||
|
||||
let motionStatus = CMMotionActivityManager.authorizationStatus()
|
||||
let pedometerStatus = CMPedometer.authorizationStatus()
|
||||
|
||||
@@ -44,11 +44,11 @@
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>OpenClaw can read recent photos when requested via the gateway.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>OpenClaw can read your contacts when requested via the gateway.</string>
|
||||
<string>OpenClaw can access your contacts when requested via the gateway.</string>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>OpenClaw can read your calendar events when requested via the gateway.</string>
|
||||
<string>OpenClaw can read and add calendar events when requested via the gateway.</string>
|
||||
<key>NSRemindersUsageDescription</key>
|
||||
<string>OpenClaw can read your reminders when requested via the gateway.</string>
|
||||
<string>OpenClaw can read and add reminders when requested via the gateway.</string>
|
||||
<key>NSMotionUsageDescription</key>
|
||||
<string>OpenClaw can read motion activity and pedometer data when requested via the gateway.</string>
|
||||
<key>NSSpeechRecognitionUsageDescription</key>
|
||||
|
||||
@@ -615,11 +615,14 @@ final class NodeAppModel {
|
||||
return try await self.handleDeviceInvoke(req)
|
||||
case OpenClawPhotosCommand.latest.rawValue:
|
||||
return try await self.handlePhotosInvoke(req)
|
||||
case OpenClawContactsCommand.search.rawValue:
|
||||
case OpenClawContactsCommand.search.rawValue,
|
||||
OpenClawContactsCommand.add.rawValue:
|
||||
return try await self.handleContactsInvoke(req)
|
||||
case OpenClawCalendarCommand.events.rawValue:
|
||||
case OpenClawCalendarCommand.events.rawValue,
|
||||
OpenClawCalendarCommand.add.rawValue:
|
||||
return try await self.handleCalendarInvoke(req)
|
||||
case OpenClawRemindersCommand.list.rawValue:
|
||||
case OpenClawRemindersCommand.list.rawValue,
|
||||
OpenClawRemindersCommand.add.rawValue:
|
||||
return try await self.handleRemindersInvoke(req)
|
||||
case OpenClawMotionCommand.activity.rawValue,
|
||||
OpenClawMotionCommand.pedometer.rawValue:
|
||||
@@ -1063,27 +1066,66 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
private func handleContactsInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(OpenClawContactsSearchParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawContactsSearchParams()
|
||||
let payload = try await self.contactsService.search(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
switch req.command {
|
||||
case OpenClawContactsCommand.search.rawValue:
|
||||
let params = (try? Self.decodeParams(OpenClawContactsSearchParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawContactsSearchParams()
|
||||
let payload = try await self.contactsService.search(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
case OpenClawContactsCommand.add.rawValue:
|
||||
let params = try Self.decodeParams(OpenClawContactsAddParams.self, from: req.paramsJSON)
|
||||
let payload = try await self.contactsService.add(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
default:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCalendarInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(OpenClawCalendarEventsParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawCalendarEventsParams()
|
||||
let payload = try await self.calendarService.events(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
switch req.command {
|
||||
case OpenClawCalendarCommand.events.rawValue:
|
||||
let params = (try? Self.decodeParams(OpenClawCalendarEventsParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawCalendarEventsParams()
|
||||
let payload = try await self.calendarService.events(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
case OpenClawCalendarCommand.add.rawValue:
|
||||
let params = try Self.decodeParams(OpenClawCalendarAddParams.self, from: req.paramsJSON)
|
||||
let payload = try await self.calendarService.add(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
default:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleRemindersInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
let params = (try? Self.decodeParams(OpenClawRemindersListParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawRemindersListParams()
|
||||
let payload = try await self.remindersService.list(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
switch req.command {
|
||||
case OpenClawRemindersCommand.list.rawValue:
|
||||
let params = (try? Self.decodeParams(OpenClawRemindersListParams.self, from: req.paramsJSON)) ??
|
||||
OpenClawRemindersListParams()
|
||||
let payload = try await self.remindersService.list(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
case OpenClawRemindersCommand.add.rawValue:
|
||||
let params = try Self.decodeParams(OpenClawRemindersAddParams.self, from: req.paramsJSON)
|
||||
let payload = try await self.remindersService.add(params: params)
|
||||
let json = try Self.encodePayload(payload)
|
||||
return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json)
|
||||
default:
|
||||
return BridgeInvokeResponse(
|
||||
id: req.id,
|
||||
ok: false,
|
||||
error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command"))
|
||||
}
|
||||
}
|
||||
|
||||
private func handleMotionInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse {
|
||||
|
||||
@@ -47,6 +47,59 @@ final class RemindersService: RemindersServicing {
|
||||
return OpenClawRemindersListPayload(reminders: payload)
|
||||
}
|
||||
|
||||
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload {
|
||||
let store = EKEventStore()
|
||||
let status = EKEventStore.authorizationStatus(for: .reminder)
|
||||
let authorized = await Self.ensureWriteAuthorization(store: store, status: status)
|
||||
guard authorized else {
|
||||
throw NSError(domain: "Reminders", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_PERMISSION_REQUIRED: grant Reminders permission",
|
||||
])
|
||||
}
|
||||
|
||||
let title = params.title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !title.isEmpty else {
|
||||
throw NSError(domain: "Reminders", code: 3, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_INVALID: title required",
|
||||
])
|
||||
}
|
||||
|
||||
let reminder = EKReminder(eventStore: store)
|
||||
reminder.title = title
|
||||
if let notes = params.notes?.trimmingCharacters(in: .whitespacesAndNewlines), !notes.isEmpty {
|
||||
reminder.notes = notes
|
||||
}
|
||||
reminder.calendar = try Self.resolveList(
|
||||
store: store,
|
||||
listId: params.listId,
|
||||
listName: params.listName)
|
||||
|
||||
if let dueISO = params.dueISO?.trimmingCharacters(in: .whitespacesAndNewlines), !dueISO.isEmpty {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
guard let dueDate = formatter.date(from: dueISO) else {
|
||||
throw NSError(domain: "Reminders", code: 4, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_INVALID: dueISO must be ISO-8601",
|
||||
])
|
||||
}
|
||||
reminder.dueDateComponents = Calendar.current.dateComponents(
|
||||
[.year, .month, .day, .hour, .minute, .second],
|
||||
from: dueDate)
|
||||
}
|
||||
|
||||
try store.save(reminder, commit: true)
|
||||
|
||||
let formatter = ISO8601DateFormatter()
|
||||
let due = reminder.dueDateComponents.flatMap { Calendar.current.date(from: $0) }
|
||||
let payload = OpenClawReminderPayload(
|
||||
identifier: reminder.calendarItemIdentifier,
|
||||
title: reminder.title,
|
||||
dueISO: due.map { formatter.string(from: $0) },
|
||||
completed: reminder.isCompleted,
|
||||
listName: reminder.calendar.title)
|
||||
|
||||
return OpenClawRemindersAddPayload(reminder: payload)
|
||||
}
|
||||
|
||||
private static func ensureAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized:
|
||||
@@ -67,4 +120,52 @@ final class RemindersService: RemindersServicing {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func ensureWriteAuthorization(store: EKEventStore, status: EKAuthorizationStatus) async -> Bool {
|
||||
switch status {
|
||||
case .authorized, .fullAccess, .writeOnly:
|
||||
return true
|
||||
case .notDetermined:
|
||||
return await withCheckedContinuation { cont in
|
||||
store.requestAccess(to: .reminder) { granted, _ in
|
||||
cont.resume(returning: granted)
|
||||
}
|
||||
}
|
||||
case .restricted, .denied:
|
||||
return false
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private static func resolveList(
|
||||
store: EKEventStore,
|
||||
listId: String?,
|
||||
listName: String?) throws -> EKCalendar
|
||||
{
|
||||
if let id = listId?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty,
|
||||
let calendar = store.calendar(withIdentifier: id)
|
||||
{
|
||||
return calendar
|
||||
}
|
||||
|
||||
if let title = listName?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
|
||||
if let calendar = store.calendars(for: .reminder).first(where: {
|
||||
$0.title.compare(title, options: [.caseInsensitive, .diacriticInsensitive]) == .orderedSame
|
||||
}) {
|
||||
return calendar
|
||||
}
|
||||
throw NSError(domain: "Reminders", code: 5, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_LIST_NOT_FOUND: no list named \(title)",
|
||||
])
|
||||
}
|
||||
|
||||
if let fallback = store.defaultCalendarForNewReminders() {
|
||||
return fallback
|
||||
}
|
||||
|
||||
throw NSError(domain: "Reminders", code: 6, userInfo: [
|
||||
NSLocalizedDescriptionKey: "REMINDERS_LIST_NOT_FOUND: no default list",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,17 @@ protocol PhotosServicing: Sendable {
|
||||
|
||||
protocol ContactsServicing: Sendable {
|
||||
func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload
|
||||
func add(params: OpenClawContactsAddParams) async throws -> OpenClawContactsAddPayload
|
||||
}
|
||||
|
||||
protocol CalendarServicing: Sendable {
|
||||
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload
|
||||
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload
|
||||
}
|
||||
|
||||
protocol RemindersServicing: Sendable {
|
||||
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload
|
||||
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload
|
||||
}
|
||||
|
||||
protocol MotionServicing: Sendable {
|
||||
|
||||
20
apps/ios/Tests/ContactsServiceTests.swift
Normal file
20
apps/ios/Tests/ContactsServiceTests.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Contacts
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite(.serialized) struct ContactsServiceTests {
|
||||
@Test func matchesPhoneOrEmailForDedupe() {
|
||||
let contact = CNMutableContact()
|
||||
contact.givenName = "Test"
|
||||
contact.phoneNumbers = [
|
||||
CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: "+1 (555) 000-0000")),
|
||||
]
|
||||
contact.emailAddresses = [
|
||||
CNLabeledValue(label: CNLabelHome, value: "test@example.com" as NSString),
|
||||
]
|
||||
|
||||
#expect(ContactsService._test_matches(contact: contact, phoneNumbers: ["15550000000"], emails: []))
|
||||
#expect(ContactsService._test_matches(contact: contact, phoneNumbers: [], emails: ["TEST@example.com"]))
|
||||
#expect(!ContactsService._test_matches(contact: contact, phoneNumbers: ["999"], emails: ["nope@example.com"]))
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,9 @@ private func withUserDefaults<T>(_ updates: [String: Any?], _ body: () throws ->
|
||||
|
||||
#expect(commands.contains(OpenClawDeviceCommand.status.rawValue))
|
||||
#expect(commands.contains(OpenClawDeviceCommand.info.rawValue))
|
||||
#expect(commands.contains(OpenClawContactsCommand.add.rawValue))
|
||||
#expect(commands.contains(OpenClawCalendarCommand.add.rawValue))
|
||||
#expect(commands.contains(OpenClawRemindersCommand.add.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -108,18 +108,24 @@ private struct TestPhotosService: PhotosServicing {
|
||||
}
|
||||
|
||||
private struct TestContactsService: ContactsServicing {
|
||||
let payload: OpenClawContactsSearchPayload
|
||||
func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload { payload }
|
||||
let searchPayload: OpenClawContactsSearchPayload
|
||||
let addPayload: OpenClawContactsAddPayload
|
||||
func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload { searchPayload }
|
||||
func add(params: OpenClawContactsAddParams) async throws -> OpenClawContactsAddPayload { addPayload }
|
||||
}
|
||||
|
||||
private struct TestCalendarService: CalendarServicing {
|
||||
let payload: OpenClawCalendarEventsPayload
|
||||
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload { payload }
|
||||
let eventsPayload: OpenClawCalendarEventsPayload
|
||||
let addPayload: OpenClawCalendarAddPayload
|
||||
func events(params: OpenClawCalendarEventsParams) async throws -> OpenClawCalendarEventsPayload { eventsPayload }
|
||||
func add(params: OpenClawCalendarAddParams) async throws -> OpenClawCalendarAddPayload { addPayload }
|
||||
}
|
||||
|
||||
private struct TestRemindersService: RemindersServicing {
|
||||
let payload: OpenClawRemindersListPayload
|
||||
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload { payload }
|
||||
let listPayload: OpenClawRemindersListPayload
|
||||
let addPayload: OpenClawRemindersAddPayload
|
||||
func list(params: OpenClawRemindersListParams) async throws -> OpenClawRemindersListPayload { listPayload }
|
||||
func add(params: OpenClawRemindersAddParams) async throws -> OpenClawRemindersAddPayload { addPayload }
|
||||
}
|
||||
|
||||
private struct TestMotionService: MotionServicing {
|
||||
@@ -316,13 +322,41 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
appVersion: "dev",
|
||||
appBuild: "0",
|
||||
locale: "en-US"))
|
||||
let emptyContact = OpenClawContactPayload(
|
||||
identifier: "c0",
|
||||
displayName: "",
|
||||
givenName: "",
|
||||
familyName: "",
|
||||
organizationName: "",
|
||||
phoneNumbers: [],
|
||||
emails: [])
|
||||
let emptyEvent = OpenClawCalendarEventPayload(
|
||||
identifier: "e0",
|
||||
title: "Test",
|
||||
startISO: "2024-01-01T00:00:00Z",
|
||||
endISO: "2024-01-01T00:30:00Z",
|
||||
isAllDay: false,
|
||||
location: nil,
|
||||
calendarTitle: nil)
|
||||
let emptyReminder = OpenClawReminderPayload(
|
||||
identifier: "r0",
|
||||
title: "Test",
|
||||
dueISO: nil,
|
||||
completed: false,
|
||||
listName: nil)
|
||||
let appModel = makeTestAppModel(
|
||||
notificationCenter: notifier,
|
||||
deviceStatusService: deviceStatus,
|
||||
photosService: TestPhotosService(payload: OpenClawPhotosLatestPayload(photos: [])),
|
||||
contactsService: TestContactsService(payload: OpenClawContactsSearchPayload(contacts: [])),
|
||||
calendarService: TestCalendarService(payload: OpenClawCalendarEventsPayload(events: [])),
|
||||
remindersService: TestRemindersService(payload: OpenClawRemindersListPayload(reminders: [])),
|
||||
contactsService: TestContactsService(
|
||||
searchPayload: OpenClawContactsSearchPayload(contacts: []),
|
||||
addPayload: OpenClawContactsAddPayload(contact: emptyContact)),
|
||||
calendarService: TestCalendarService(
|
||||
eventsPayload: OpenClawCalendarEventsPayload(events: []),
|
||||
addPayload: OpenClawCalendarAddPayload(event: emptyEvent)),
|
||||
remindersService: TestRemindersService(
|
||||
listPayload: OpenClawRemindersListPayload(reminders: []),
|
||||
addPayload: OpenClawRemindersAddPayload(reminder: emptyReminder)),
|
||||
motionService: TestMotionService(
|
||||
activityPayload: OpenClawMotionActivityPayload(activities: []),
|
||||
pedometerPayload: OpenClawPedometerPayload(
|
||||
@@ -383,6 +417,15 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
phoneNumbers: ["+1"],
|
||||
emails: ["jane@example.com"]),
|
||||
])
|
||||
let contactsAddPayload = OpenClawContactsAddPayload(
|
||||
contact: OpenClawContactPayload(
|
||||
identifier: "c2",
|
||||
displayName: "Added",
|
||||
givenName: "Added",
|
||||
familyName: "",
|
||||
organizationName: "",
|
||||
phoneNumbers: ["+2"],
|
||||
emails: ["add@example.com"]))
|
||||
let calendarPayload = OpenClawCalendarEventsPayload(
|
||||
events: [
|
||||
OpenClawCalendarEventPayload(
|
||||
@@ -394,6 +437,15 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
location: nil,
|
||||
calendarTitle: "Work"),
|
||||
])
|
||||
let calendarAddPayload = OpenClawCalendarAddPayload(
|
||||
event: OpenClawCalendarEventPayload(
|
||||
identifier: "e2",
|
||||
title: "Added Event",
|
||||
startISO: "2024-01-02T00:00:00Z",
|
||||
endISO: "2024-01-02T01:00:00Z",
|
||||
isAllDay: false,
|
||||
location: "HQ",
|
||||
calendarTitle: "Work"))
|
||||
let remindersPayload = OpenClawRemindersListPayload(
|
||||
reminders: [
|
||||
OpenClawReminderPayload(
|
||||
@@ -403,6 +455,13 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
completed: false,
|
||||
listName: "Inbox"),
|
||||
])
|
||||
let remindersAddPayload = OpenClawRemindersAddPayload(
|
||||
reminder: OpenClawReminderPayload(
|
||||
identifier: "r2",
|
||||
title: "Added Reminder",
|
||||
dueISO: "2024-01-03T00:00:00Z",
|
||||
completed: false,
|
||||
listName: "Inbox"))
|
||||
let motionPayload = OpenClawMotionActivityPayload(
|
||||
activities: [
|
||||
OpenClawMotionActivityEntry(
|
||||
@@ -429,9 +488,15 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
statusPayload: deviceStatusPayload,
|
||||
infoPayload: deviceInfoPayload),
|
||||
photosService: TestPhotosService(payload: photosPayload),
|
||||
contactsService: TestContactsService(payload: contactsPayload),
|
||||
calendarService: TestCalendarService(payload: calendarPayload),
|
||||
remindersService: TestRemindersService(payload: remindersPayload),
|
||||
contactsService: TestContactsService(
|
||||
searchPayload: contactsPayload,
|
||||
addPayload: contactsAddPayload),
|
||||
calendarService: TestCalendarService(
|
||||
eventsPayload: calendarPayload,
|
||||
addPayload: calendarAddPayload),
|
||||
remindersService: TestRemindersService(
|
||||
listPayload: remindersPayload,
|
||||
addPayload: remindersAddPayload),
|
||||
motionService: TestMotionService(
|
||||
activityPayload: motionPayload,
|
||||
pedometerPayload: pedometerPayload))
|
||||
@@ -460,18 +525,62 @@ private func decodePayload<T: Decodable>(_ json: String?, as type: T.Type) throw
|
||||
let decodedContacts = try decodePayload(contactsRes.payloadJSON, as: OpenClawContactsSearchPayload.self)
|
||||
#expect(decodedContacts == contactsPayload)
|
||||
|
||||
let contactsAddParams = OpenClawContactsAddParams(
|
||||
givenName: "Added",
|
||||
phoneNumbers: ["+2"],
|
||||
emails: ["add@example.com"])
|
||||
let contactsAddData = try JSONEncoder().encode(contactsAddParams)
|
||||
let contactsAddReq = BridgeInvokeRequest(
|
||||
id: "contacts-add",
|
||||
command: OpenClawContactsCommand.add.rawValue,
|
||||
paramsJSON: String(decoding: contactsAddData, as: UTF8.self))
|
||||
let contactsAddRes = await appModel._test_handleInvoke(contactsAddReq)
|
||||
#expect(contactsAddRes.ok == true)
|
||||
let decodedContactsAdd = try decodePayload(contactsAddRes.payloadJSON, as: OpenClawContactsAddPayload.self)
|
||||
#expect(decodedContactsAdd == contactsAddPayload)
|
||||
|
||||
let calendarReq = BridgeInvokeRequest(id: "calendar", command: OpenClawCalendarCommand.events.rawValue)
|
||||
let calendarRes = await appModel._test_handleInvoke(calendarReq)
|
||||
#expect(calendarRes.ok == true)
|
||||
let decodedCalendar = try decodePayload(calendarRes.payloadJSON, as: OpenClawCalendarEventsPayload.self)
|
||||
#expect(decodedCalendar == calendarPayload)
|
||||
|
||||
let calendarAddParams = OpenClawCalendarAddParams(
|
||||
title: "Added Event",
|
||||
startISO: "2024-01-02T00:00:00Z",
|
||||
endISO: "2024-01-02T01:00:00Z",
|
||||
location: "HQ",
|
||||
calendarTitle: "Work")
|
||||
let calendarAddData = try JSONEncoder().encode(calendarAddParams)
|
||||
let calendarAddReq = BridgeInvokeRequest(
|
||||
id: "calendar-add",
|
||||
command: OpenClawCalendarCommand.add.rawValue,
|
||||
paramsJSON: String(decoding: calendarAddData, as: UTF8.self))
|
||||
let calendarAddRes = await appModel._test_handleInvoke(calendarAddReq)
|
||||
#expect(calendarAddRes.ok == true)
|
||||
let decodedCalendarAdd = try decodePayload(calendarAddRes.payloadJSON, as: OpenClawCalendarAddPayload.self)
|
||||
#expect(decodedCalendarAdd == calendarAddPayload)
|
||||
|
||||
let remindersReq = BridgeInvokeRequest(id: "reminders", command: OpenClawRemindersCommand.list.rawValue)
|
||||
let remindersRes = await appModel._test_handleInvoke(remindersReq)
|
||||
#expect(remindersRes.ok == true)
|
||||
let decodedReminders = try decodePayload(remindersRes.payloadJSON, as: OpenClawRemindersListPayload.self)
|
||||
#expect(decodedReminders == remindersPayload)
|
||||
|
||||
let remindersAddParams = OpenClawRemindersAddParams(
|
||||
title: "Added Reminder",
|
||||
dueISO: "2024-01-03T00:00:00Z",
|
||||
listName: "Inbox")
|
||||
let remindersAddData = try JSONEncoder().encode(remindersAddParams)
|
||||
let remindersAddReq = BridgeInvokeRequest(
|
||||
id: "reminders-add",
|
||||
command: OpenClawRemindersCommand.add.rawValue,
|
||||
paramsJSON: String(decoding: remindersAddData, as: UTF8.self))
|
||||
let remindersAddRes = await appModel._test_handleInvoke(remindersAddReq)
|
||||
#expect(remindersAddRes.ok == true)
|
||||
let decodedRemindersAdd = try decodePayload(remindersAddRes.payloadJSON, as: OpenClawRemindersAddPayload.self)
|
||||
#expect(decodedRemindersAdd == remindersAddPayload)
|
||||
|
||||
let motionReq = BridgeInvokeRequest(id: "motion", command: OpenClawMotionCommand.activity.rawValue)
|
||||
let motionRes = await appModel._test_handleInvoke(motionReq)
|
||||
#expect(motionRes.ok == true)
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
|
||||
public enum OpenClawCalendarCommand: String, Codable, Sendable {
|
||||
case events = "calendar.events"
|
||||
case add = "calendar.add"
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarEventsParams: Codable, Sendable, Equatable {
|
||||
@@ -16,6 +17,37 @@ public struct OpenClawCalendarEventsParams: Codable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarAddParams: Codable, Sendable, Equatable {
|
||||
public var title: String
|
||||
public var startISO: String
|
||||
public var endISO: String
|
||||
public var isAllDay: Bool?
|
||||
public var location: String?
|
||||
public var notes: String?
|
||||
public var calendarId: String?
|
||||
public var calendarTitle: String?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
startISO: String,
|
||||
endISO: String,
|
||||
isAllDay: Bool? = nil,
|
||||
location: String? = nil,
|
||||
notes: String? = nil,
|
||||
calendarId: String? = nil,
|
||||
calendarTitle: String? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.startISO = startISO
|
||||
self.endISO = endISO
|
||||
self.isAllDay = isAllDay
|
||||
self.location = location
|
||||
self.notes = notes
|
||||
self.calendarId = calendarId
|
||||
self.calendarTitle = calendarTitle
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarEventPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var title: String
|
||||
@@ -51,3 +83,11 @@ public struct OpenClawCalendarEventsPayload: Codable, Sendable, Equatable {
|
||||
self.events = events
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawCalendarAddPayload: Codable, Sendable, Equatable {
|
||||
public var event: OpenClawCalendarEventPayload
|
||||
|
||||
public init(event: OpenClawCalendarEventPayload) {
|
||||
self.event = event
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
|
||||
public enum OpenClawContactsCommand: String, Codable, Sendable {
|
||||
case search = "contacts.search"
|
||||
case add = "contacts.add"
|
||||
}
|
||||
|
||||
public struct OpenClawContactsSearchParams: Codable, Sendable, Equatable {
|
||||
@@ -14,6 +15,31 @@ public struct OpenClawContactsSearchParams: Codable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactsAddParams: Codable, Sendable, Equatable {
|
||||
public var givenName: String?
|
||||
public var familyName: String?
|
||||
public var organizationName: String?
|
||||
public var displayName: String?
|
||||
public var phoneNumbers: [String]?
|
||||
public var emails: [String]?
|
||||
|
||||
public init(
|
||||
givenName: String? = nil,
|
||||
familyName: String? = nil,
|
||||
organizationName: String? = nil,
|
||||
displayName: String? = nil,
|
||||
phoneNumbers: [String]? = nil,
|
||||
emails: [String]? = nil)
|
||||
{
|
||||
self.givenName = givenName
|
||||
self.familyName = familyName
|
||||
self.organizationName = organizationName
|
||||
self.displayName = displayName
|
||||
self.phoneNumbers = phoneNumbers
|
||||
self.emails = emails
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var displayName: String
|
||||
@@ -49,3 +75,11 @@ public struct OpenClawContactsSearchPayload: Codable, Sendable, Equatable {
|
||||
self.contacts = contacts
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawContactsAddPayload: Codable, Sendable, Equatable {
|
||||
public var contact: OpenClawContactPayload
|
||||
|
||||
public init(contact: OpenClawContactPayload) {
|
||||
self.contact = contact
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Foundation
|
||||
|
||||
public enum OpenClawRemindersCommand: String, Codable, Sendable {
|
||||
case list = "reminders.list"
|
||||
case add = "reminders.add"
|
||||
}
|
||||
|
||||
public enum OpenClawReminderStatusFilter: String, Codable, Sendable {
|
||||
@@ -20,6 +21,28 @@ public struct OpenClawRemindersListParams: Codable, Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersAddParams: Codable, Sendable, Equatable {
|
||||
public var title: String
|
||||
public var dueISO: String?
|
||||
public var notes: String?
|
||||
public var listId: String?
|
||||
public var listName: String?
|
||||
|
||||
public init(
|
||||
title: String,
|
||||
dueISO: String? = nil,
|
||||
notes: String? = nil,
|
||||
listId: String? = nil,
|
||||
listName: String? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.dueISO = dueISO
|
||||
self.notes = notes
|
||||
self.listId = listId
|
||||
self.listName = listName
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawReminderPayload: Codable, Sendable, Equatable {
|
||||
public var identifier: String
|
||||
public var title: String
|
||||
@@ -49,3 +72,11 @@ public struct OpenClawRemindersListPayload: Codable, Sendable, Equatable {
|
||||
self.reminders = reminders
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawRemindersAddPayload: Codable, Sendable, Equatable {
|
||||
public var reminder: OpenClawReminderPayload
|
||||
|
||||
public init(reminder: OpenClawReminderPayload) {
|
||||
self.reminder = reminder
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,20 @@ const LOCATION_COMMANDS = ["location.get"];
|
||||
|
||||
const SMS_COMMANDS = ["sms.send"];
|
||||
|
||||
const DEVICE_COMMANDS = ["device.status", "device.info"];
|
||||
|
||||
const PHOTOS_COMMANDS = ["photos.latest"];
|
||||
|
||||
const CONTACTS_COMMANDS = ["contacts.search", "contacts.add"];
|
||||
|
||||
const CALENDAR_COMMANDS = ["calendar.events", "calendar.add"];
|
||||
|
||||
const REMINDERS_COMMANDS = ["reminders.list", "reminders.add"];
|
||||
|
||||
const MOTION_COMMANDS = ["motion.activity", "motion.pedometer"];
|
||||
|
||||
const SYSTEM_NOTIFY_COMMANDS = ["system.notify"];
|
||||
|
||||
const SYSTEM_COMMANDS = [
|
||||
"system.run",
|
||||
"system.which",
|
||||
@@ -30,7 +44,19 @@ const SYSTEM_COMMANDS = [
|
||||
];
|
||||
|
||||
const PLATFORM_DEFAULTS: Record<string, string[]> = {
|
||||
ios: [...CANVAS_COMMANDS, ...CAMERA_COMMANDS, ...SCREEN_COMMANDS, ...LOCATION_COMMANDS],
|
||||
ios: [
|
||||
...CANVAS_COMMANDS,
|
||||
...CAMERA_COMMANDS,
|
||||
...SCREEN_COMMANDS,
|
||||
...LOCATION_COMMANDS,
|
||||
...SYSTEM_NOTIFY_COMMANDS,
|
||||
...DEVICE_COMMANDS,
|
||||
...PHOTOS_COMMANDS,
|
||||
...CONTACTS_COMMANDS,
|
||||
...CALENDAR_COMMANDS,
|
||||
...REMINDERS_COMMANDS,
|
||||
...MOTION_COMMANDS,
|
||||
],
|
||||
android: [
|
||||
...CANVAS_COMMANDS,
|
||||
...CAMERA_COMMANDS,
|
||||
|
||||
Reference in New Issue
Block a user