From be9c5ffb16e5082ce24b9bc1d270805c62fc0145 Mon Sep 17 00:00:00 2001 From: toonvl Date: Thu, 9 Jul 2026 12:00:31 +0200 Subject: [PATCH] Add organizer filter and annotation to events_fetch Expose whether the current user owns a calendar event or was invited to it by someone else, so callers can tell self-created events apart from shared invites such as colleagues' vacation leave. - EKEvent.isOrganizedByCurrentUser / organizerDisplayName helpers - events_fetch: new `organizer: "me" | "others"` filter - every returned event now carries `organizedByMe` (and `organizer` name when someone else owns it), enriched at the Value layer so the remote Ontology.Event model is left untouched Co-Authored-By: Claude Opus 4.8 --- App/Extensions/EventKit+Extensions.swift | 24 ++++++++++++++++ App/Services/Calendar.swift | 36 +++++++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/App/Extensions/EventKit+Extensions.swift b/App/Extensions/EventKit+Extensions.swift index 2c247d02..077a2d58 100644 --- a/App/Extensions/EventKit+Extensions.swift +++ b/App/Extensions/EventKit+Extensions.swift @@ -38,6 +38,30 @@ extension EKEventStatus { } } +extension EKEvent { + /// Whether the current user organizes/owns this event, as opposed to having + /// been invited to it by someone else. + /// + /// An event with no organizer (e.g. a plain event you created, a birthday, or + /// a subscribed holiday) is treated as your own. An event that has an organizer + /// is yours only if that organizer is the current user; otherwise it was created + /// by someone else and shared/invited to you (e.g. a colleague's leave). + var isOrganizedByCurrentUser: Bool { + guard let organizer = organizer else { return true } + return organizer.isCurrentUser + } + + /// Display name of the organizer, if the event was organized by someone else. + /// Returns `nil` for events you own or when no organizer information is available. + var organizerDisplayName: String? { + guard let organizer = organizer, !organizer.isCurrentUser else { return nil } + return organizer.name ?? organizer.url.absoluteString.replacingOccurrences( + of: "mailto:", + with: "" + ) + } +} + extension EKRecurrenceFrequency { init(_ string: String) { switch string.lowercased() { diff --git a/App/Services/Calendar.swift b/App/Services/Calendar.swift index 1a58890d..30f645e9 100644 --- a/App/Services/Calendar.swift +++ b/App/Services/Calendar.swift @@ -94,6 +94,11 @@ final class CalendarService: Service { ), "hasAlarms": .boolean(), "isRecurring": .boolean(), + "organizer": .string( + description: + "Filter by who organized the event: 'me' for events you created or organize (including personal events with no other participants, birthdays, and subscribed holidays), 'others' for events someone else invited you to (e.g. a colleague's shared vacation/leave). Omit to include both.", + enum: ["me", "others"] + ), ], additionalProperties: false ), @@ -216,7 +221,36 @@ final class CalendarService: Service { events = events.filter { ($0.hasRecurrenceRules) == isRecurring } } - return events.map { Event($0) } + if case .string(let organizer) = arguments["organizer"] { + switch organizer { + case "me": + events = events.filter { $0.isOrganizedByCurrentUser } + case "others": + events = events.filter { !$0.isOrganizedByCurrentUser } + default: + break + } + } + + // Encode each event and annotate it with organizer information so callers + // can distinguish events the user owns from ones they were invited to. + let encoder = JSONEncoder() + encoder.userInfo[Ontology.DateTime.timeZoneOverrideKey] = TimeZone.current + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + let decoder = JSONDecoder() + + return try events.map { ekEvent -> Value in + let data = try encoder.encode(Event(ekEvent)) + var value = try decoder.decode(Value.self, from: data) + if case .object(var object) = value { + object["organizedByMe"] = .bool(ekEvent.isOrganizedByCurrentUser) + if let organizerName = ekEvent.organizerDisplayName { + object["organizer"] = .string(organizerName) + } + value = .object(object) + } + return value + } } Tool( name: "events_create",