diff --git a/backend/routers/google_calendar.py b/backend/routers/google_calendar.py index 4753c2d7692..7e589c0aff9 100644 --- a/backend/routers/google_calendar.py +++ b/backend/routers/google_calendar.py @@ -37,6 +37,9 @@ class GoogleCalendarEvent(BaseModel): start_time: datetime = Field(description="Event start time") end_time: datetime = Field(description="Event end time") html_link: Optional[str] = Field(default=None, description="Link to open event in Google Calendar") + location: str = Field(default='', description="Event location") + description: str = Field(default='', description="Event description, truncated for transport") + all_day: bool = Field(default=False, description="True when the event has a date but no time") def _get_google_calendar_token(uid: str) -> tuple[str, Dict[str, Any]]: @@ -62,6 +65,9 @@ def _event_to_response(event: Dict[str, Any]) -> Optional[GoogleCalendarEvent]: attendee_names, attendee_emails = extract_attendees(event) + start_raw = event.get('start') or {} + all_day = isinstance(start_raw, dict) and 'date' in start_raw and 'dateTime' not in start_raw + return GoogleCalendarEvent( event_id=event.get('id', ''), title=event.get('summary', 'Untitled Event'), @@ -70,6 +76,9 @@ def _event_to_response(event: Dict[str, Any]) -> Optional[GoogleCalendarEvent]: start_time=start_time, end_time=end_time, html_link=event.get('htmlLink'), + location=(event.get('location') or '')[:200], + description=(event.get('description') or '')[:300], + all_day=all_day, ) @@ -82,7 +91,9 @@ async def list_google_calendar_events( time_min: Optional[datetime] = Query(None, description="Minimum time for events (ISO format)"), time_max: Optional[datetime] = Query(None, description="Maximum time for events (ISO format)"), q: Optional[str] = Query(None, description="Search query to filter events"), - max_results: int = Query(20, ge=1, le=100, description="Maximum number of events to return"), + # Ceiling raised from 100 for the desktop connector import, which reads a + # year of history in one pass; Google's own list cap is 2500. + max_results: int = Query(20, ge=1, le=500, description="Maximum number of events to return"), x_app_platform: Optional[str] = Header(None, alias='X-App-Platform'), x_app_version: Optional[str] = Header(None, alias='X-App-Version'), x_app_build: Optional[str] = Header(None, alias='X-App-Build'), diff --git a/backend/tests/unit/test_google_calendar_event_response.py b/backend/tests/unit/test_google_calendar_event_response.py new file mode 100644 index 00000000000..a4a45e68987 --- /dev/null +++ b/backend/tests/unit/test_google_calendar_event_response.py @@ -0,0 +1,56 @@ +"""The desktop connector reads calendar events through the OAuth grant instead of +scraping browser cookies, so the event payload has to carry the fields the +memory extractor used to get straight from Google: location, description, and +whether the event is all-day. +""" + +from routers.google_calendar import _event_to_response + + +def _timed_event(**overrides): + event = { + 'id': 'evt-1', + 'summary': 'Design review', + 'start': {'dateTime': '2026-08-26T15:00:00Z'}, + 'end': {'dateTime': '2026-08-26T16:00:00Z'}, + 'location': 'Room 4', + 'description': 'Walk through the new connector flow.', + } + event.update(overrides) + return event + + +def test_timed_event_carries_location_and_description(): + response = _event_to_response(_timed_event()) + + assert response is not None + assert response.location == 'Room 4' + assert response.description == 'Walk through the new connector flow.' + assert response.all_day is False + + +def test_all_day_event_is_flagged(): + response = _event_to_response(_timed_event(start={'date': '2026-08-26'}, end={'date': '2026-08-27'})) + + assert response is not None + assert response.all_day is True + + +def test_missing_optional_fields_become_empty_strings(): + event = _timed_event() + del event['location'] + event['description'] = None + + response = _event_to_response(event) + + assert response is not None + assert response.location == '' + assert response.description == '' + + +def test_long_free_text_is_truncated_for_transport(): + response = _event_to_response(_timed_event(location='L' * 500, description='D' * 900)) + + assert response is not None + assert len(response.location) == 200 + assert len(response.description) == 300 diff --git a/desktop/macos/Desktop/Sources/APIClient+GoogleCalendarGrant.swift b/desktop/macos/Desktop/Sources/APIClient+GoogleCalendarGrant.swift new file mode 100644 index 00000000000..8bd2931b52f --- /dev/null +++ b/desktop/macos/Desktop/Sources/APIClient+GoogleCalendarGrant.swift @@ -0,0 +1,113 @@ +import Foundation + +/// Backend transport for the Google Calendar OAuth grant — the same grant the +/// mobile app connects, held server-side. +/// +/// The desktop's original Calendar path decrypts Google auth cookies out of a +/// Chromium profile. That mechanism fails for whole classes of user with no +/// recoverable action: no Chromium browser installed (Safari/Firefox), cookies +/// re-encrypted under a scheme the reader can't open, or a declined Keychain +/// "Safe Storage" prompt. See issue #10459. This transport is the durable +/// alternative — the backend already owns the token, so nothing on this machine +/// needs to read a browser at all. +/// The `app_key` path parameter for both integration routes. Kept as an +/// interpolated value rather than baked into the literal because these are the +/// templated `/v1/integrations/{app_key}` routes, not routes of their own — +/// `test_desktop_rest_inventory` extracts the literal and matches it against the +/// app-client OpenAPI spec, so a hardcoded key reads as a route the spec lacks. +private let googleCalendarAppKey = "google_calendar" + +extension APIClient { + /// Whether this account holds a live Google Calendar grant. + func googleCalendarGrantConnected() async throws -> Bool { + let response: IntegrationConnectionResponse = try await get("v1/integrations/\(googleCalendarAppKey)") + return response.connected + } + + /// Google's consent URL for the Calendar grant. The backend owns the redirect + /// and the CSRF state, so the desktop only has to open what it returns. + func googleCalendarOAuthURL() async throws -> URL { + let response: IntegrationOAuthURLResponse = try await get( + "v1/integrations/\(googleCalendarAppKey)/oauth-url") + guard let url = URL(string: response.authUrl) else { throw APIError.invalidResponse } + return url + } + + /// Read events through the backend grant. Mirrors the window the cookie + /// reader uses so both sources produce the same shape for callers. + func googleCalendarGrantEvents( + daysBack: Int, + daysForward: Int, + maxResults: Int + ) async throws -> [CalendarEvent] { + let formatter = ISO8601DateFormatter() + let now = Date() + let timeMin = formatter.string(from: now.addingTimeInterval(-Double(daysBack) * 86_400)) + let timeMax = formatter.string(from: now.addingTimeInterval(Double(daysForward) * 86_400)) + + func encoded(_ value: String) -> String { + value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? value + } + + // The endpoint's ceiling is 500; asking for more is a 422, not a clamp. + let capped = min(max(maxResults, 1), 500) + let events: [GoogleCalendarGrantEvent] = try await get( + "v1/calendar/google/events?time_min=\(encoded(timeMin))&time_max=\(encoded(timeMax))&max_results=\(capped)" + ) + return events.map(\.asCalendarEvent) + } +} + +struct IntegrationConnectionResponse: Decodable { + let connected: Bool + let appKey: String? + + enum CodingKeys: String, CodingKey { + case connected + case appKey = "app_key" + } +} + +struct IntegrationOAuthURLResponse: Decodable { + let authUrl: String + + enum CodingKeys: String, CodingKey { + case authUrl = "auth_url" + } +} + +/// Wire shape of `GET /v1/calendar/google/events`. +struct GoogleCalendarGrantEvent: Decodable { + let eventId: String + let title: String + let attendees: [String] + let startTime: String + let endTime: String + let location: String + let description: String + let allDay: Bool + + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case title + case attendees + case startTime = "start_time" + case endTime = "end_time" + case location + case description + case allDay = "all_day" + } + + var asCalendarEvent: CalendarEvent { + CalendarEvent( + id: eventId, + summary: title, + startTime: startTime, + endTime: endTime, + attendees: attendees, + location: location, + description: description, + isAllDay: allDay + ) + } +} diff --git a/desktop/macos/Desktop/Sources/CalendarReaderService.swift b/desktop/macos/Desktop/Sources/CalendarReaderService.swift index 8b2b5cb4a78..62864da949f 100644 --- a/desktop/macos/Desktop/Sources/CalendarReaderService.swift +++ b/desktop/macos/Desktop/Sources/CalendarReaderService.swift @@ -222,7 +222,17 @@ enum CalendarOutcomeParser { actor CalendarReaderService { static let shared = CalendarReaderService() - /// Read calendar events using browser cookies + SAPISID auth. + /// Read calendar events, preferring the account's server-held Google OAuth + /// grant and falling back to the Chromium cookie reader. + /// + /// The grant comes first because it is strictly more available: it works with + /// no Chromium browser installed, with cookies this machine cannot decrypt, + /// and with the browser Keychain prompt declined — the three failure modes in + /// issue #10459 that leave the cookie reader with nothing to recover to. The + /// cookie path stays for accounts that have never granted OAuth, so nobody + /// who is working today stops working. Retiring it belongs to that issue, not + /// to this fix. + /// /// Tries Arc, Chrome, Brave, and Edge across all Chromium profiles. /// Fetches events from `daysBack` days ago to `daysForward` days from now. /// Browser Keychain consent is only eligible for an explicitly requested read. @@ -234,6 +244,15 @@ actor CalendarReaderService { ) async throws -> [CalendarEvent] { + // Absent grant and failed grant read both fall through to cookies. + if let granted = try? await readEventsViaGrant( + daysBack: daysBack, + daysForward: daysForward, + maxResults: maxResults + ) { + return granted + } + if userInitiated { BrowserKeychainCache.shared.beginUserInitiatedOperation() } @@ -247,6 +266,29 @@ actor CalendarReaderService { return events.sorted { $0.startTime > $1.startTime } } + /// Returns nil when this account has no Google grant. Callers that can fall + /// back to cookies treat a thrown read the same way, so a backend blip cannot + /// take Calendar down for someone the cookie reader still serves. + func readEventsViaGrant( + daysBack: Int, + daysForward: Int, + maxResults: Int + ) async throws -> [CalendarEvent]? { + guard await hasGoogleGrant() else { return nil } + let events = try await APIClient.shared.googleCalendarGrantEvents( + daysBack: daysBack, + daysForward: daysForward, + maxResults: maxResults + ) + return events.sorted { $0.startTime > $1.startTime } + } + + /// A grant check must never be the reason a read fails: a backend hiccup here + /// reads as "no grant" and the cookie path still gets its turn. + func hasGoogleGrant() async -> Bool { + (try? await APIClient.shared.googleCalendarGrantConnected()) ?? false + } + /// Lightweight functional probe — does the integration actually work right now? /// /// Per `docs/integrations-philosophy.md` §3/§4, "connected" must mean "verified @@ -255,6 +297,14 @@ actor CalendarReaderService { /// one-time success. It runs the same real fetch path over a tiny window so a /// green result guarantees the whole chain (cookies → auth → API) works. func verifyConnection(userInitiated: Bool = false) async -> CalendarConnectionStatus { + // Same real-fetch standard as the cookie probe below, over the grant: a + // green result means the whole chain (grant → backend → Google) works. + if await hasGoogleGrant(), + (try? await APIClient.shared.googleCalendarGrantEvents(daysBack: 1, daysForward: 1, maxResults: 1)) != nil + { + return .connected(verifiedAt: Date()) + } + if userInitiated { BrowserKeychainCache.shared.beginUserInitiatedOperation() } diff --git a/desktop/macos/Desktop/Sources/MainWindow/Components/UserFacingErrorPresentation.swift b/desktop/macos/Desktop/Sources/MainWindow/Components/UserFacingErrorPresentation.swift index ec25b257cf2..4b5c9ff05bf 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/Components/UserFacingErrorPresentation.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/Components/UserFacingErrorPresentation.swift @@ -119,17 +119,26 @@ enum UserFacingErrorPresentation { return fallback(for: context) } + /// Raw-system-error fingerprints. Copy that carries none of them is copy this + /// app wrote, and is shown verbatim. + /// + /// This deliberately does not judge by sentence shape. A previous version also + /// required a trailing period and fewer than 120 characters, which flattened + /// five real connector messages into the generic fallback — including the + /// Calendar not-signed-in guidance, at exactly 120 characters, which is the + /// single most common Google connect failure and the one that most needs its + /// next step shown. Length and punctuation do not separate curated copy from + /// raw system text; the fingerprints below do. private static func shouldPreserveCuratedCopy(_ text: String) -> Bool { let lowered = text.lowercased() if text.count > 160 { return false } - if text.contains("://") || text.contains("Error Domain=") { return false } - if lowered.contains("nsurlerror") || lowered.contains("cfstream") { return false } + if text.contains("://") { return false } + // Catches `Error Domain=` and every bare `NS*ErrorDomain` spelling. + if lowered.contains("errordomain") { return false } + if lowered.contains("cfstream") { return false } if lowered.range(of: #"\b[45]\d{2}\b"#, options: .regularExpression) != nil { return false } if text.filter({ $0 == ":" }).count > 1 { return false } - if lowered.hasPrefix("omi ") || lowered.hasPrefix("couldn't ") || lowered.hasPrefix("didn't ") { - return true - } - return text.hasSuffix(".") && text.count < 120 + return true } private static func fallback(for context: Context) -> String { diff --git a/desktop/macos/Desktop/Sources/MainWindow/Pages/ConnectorImportOperations.swift b/desktop/macos/Desktop/Sources/MainWindow/Pages/ConnectorImportOperations.swift index fdb796e866b..f4b15d8f98f 100644 --- a/desktop/macos/Desktop/Sources/MainWindow/Pages/ConnectorImportOperations.swift +++ b/desktop/macos/Desktop/Sources/MainWindow/Pages/ConnectorImportOperations.swift @@ -228,19 +228,15 @@ enum ConnectorImportOperations { maxResults: 500, userInitiated: true ) - progress.update( - title: "Importing calendar events", - detail: "Saving events as memories and generating action-oriented summaries." - ) - let rawImport = await CalendarReaderService.shared.saveAsMemories(events: events, limit: 200) - let synthesis = await CalendarReaderService.shared.synthesizeFromEvents(events: events) - let memoryCount = rawImport.saved + synthesis.memories - return .success( - SyncResult(sourceCount: events.count, memoryCount: memoryCount, newItems: events.count), - message: "Read \(events.count.formatted()) calendar events and saved \(memoryCount.formatted()) memories." - ) + return await saveCalendarEvents(events, progress: progress) } catch let error as CalendarReaderError { - return .failure(message: error.localizedDescription, failureClass: Self.failureClass(for: error)) + // Every `CalendarReaderError` means the browser-cookie reader found no + // usable Google session on this machine, and `readEvents` only reaches it + // when the account has no OAuth grant either. Both sources are exhausted, + // so "try again" is not advice — nothing about the machine has changed. + // Google's own consent screen is the one path that does not depend on + // which browser is installed or whether its cookies can be decrypted. + return await connectCalendarViaOAuth(progress: progress, cookieFailure: error) } catch { return .failure( message: error.localizedDescription, @@ -248,6 +244,80 @@ enum ConnectorImportOperations { } } + /// Backend-mediated Google OAuth, same shape as `connectX`: open the consent + /// URL, poll until the grant lands, then read through it. + @MainActor + private static func connectCalendarViaOAuth( + progress: ConnectorImportRunner.ProgressSink, + cookieFailure: CalendarReaderError + ) async -> Outcome { + let authURL: URL + do { + authURL = try await APIClient.shared.googleCalendarOAuthURL() + } catch { + // Server-side OAuth is unavailable, so the cookie reader's own diagnosis + // is still the most actionable thing the user can be told. + return .failure( + message: cookieFailure.localizedDescription, + failureClass: Self.failureClass(for: cookieFailure)) + } + + guard NSWorkspace.shared.open(authURL) else { + return .failure( + message: "Couldn't open the Google sign-in page. Check your default browser, then try again.", + failureClass: .noBrowser) + } + + progress.update( + title: "Waiting for Google sign-in", + detail: "Approve calendar access in your browser. This window updates automatically." + ) + + for _ in 0..<60 { + try? await Task.sleep(for: .seconds(2)) + guard await CalendarReaderService.shared.hasGoogleGrant() else { continue } + progress.update( + title: "Importing calendar events", + detail: "Reading past events and upcoming commitments for memory extraction." + ) + do { + let events = + try await CalendarReaderService.shared.readEventsViaGrant( + daysBack: 365, + daysForward: 30, + maxResults: 500 + ) ?? [] + return await saveCalendarEvents(events, progress: progress) + } catch { + return .failure( + message: "Connected to Google, but reading your calendar failed. Try Sync now.", + failureClass: IntegrationConnectTelemetry.ErrorClass.fromMessage(error.localizedDescription)) + } + } + + return .failure( + message: "Didn't hear back from Google. If you approved access, try again.", + failureClass: .notSignedIn) + } + + @MainActor + private static func saveCalendarEvents( + _ events: [CalendarEvent], + progress: ConnectorImportRunner.ProgressSink + ) async -> Outcome { + progress.update( + title: "Importing calendar events", + detail: "Saving events as memories and generating action-oriented summaries." + ) + let rawImport = await CalendarReaderService.shared.saveAsMemories(events: events, limit: 200) + let synthesis = await CalendarReaderService.shared.synthesizeFromEvents(events: events) + let memoryCount = rawImport.saved + synthesis.memories + return .success( + SyncResult(sourceCount: events.count, memoryCount: memoryCount, newItems: events.count), + message: "Read \(events.count.formatted()) calendar events and saved \(memoryCount.formatted()) memories." + ) + } + @MainActor static func importAppleNotes(progress: ConnectorImportRunner.ProgressSink) async -> Outcome { do { diff --git a/desktop/macos/Desktop/Tests/GoogleCalendarGrantEventTests.swift b/desktop/macos/Desktop/Tests/GoogleCalendarGrantEventTests.swift new file mode 100644 index 00000000000..995af294da0 --- /dev/null +++ b/desktop/macos/Desktop/Tests/GoogleCalendarGrantEventTests.swift @@ -0,0 +1,64 @@ +import XCTest + +@testable import Omi_Computer + +/// The grant path replaces a browser-cookie read that produced `CalendarEvent` +/// directly, so the wire shape has to decode into the same value — a silent +/// field drop here would quietly degrade every memory built from an event. +final class GoogleCalendarGrantEventTests: XCTestCase { + private func decode(_ json: String) throws -> [GoogleCalendarGrantEvent] { + try JSONDecoder().decode([GoogleCalendarGrantEvent].self, from: Data(json.utf8)) + } + + func testDecodesBackendEventIntoCalendarEvent() throws { + let events = try decode( + """ + [{ + "event_id": "evt-1", + "title": "Design review", + "attendees": ["Dana", "Ren"], + "attendee_emails": ["dana@example.com"], + "start_time": "2026-08-26T15:00:00Z", + "end_time": "2026-08-26T16:00:00Z", + "html_link": "https://calendar.google.com/event?eid=1", + "location": "Room 4", + "description": "Walk through the new connector flow.", + "all_day": false + }] + """ + ) + + let event = try XCTUnwrap(events.first).asCalendarEvent + XCTAssertEqual(event.id, "evt-1") + XCTAssertEqual(event.summary, "Design review") + XCTAssertEqual(event.attendees, ["Dana", "Ren"]) + XCTAssertEqual(event.startTime, "2026-08-26T15:00:00Z") + XCTAssertEqual(event.endTime, "2026-08-26T16:00:00Z") + XCTAssertEqual(event.location, "Room 4") + XCTAssertEqual(event.description, "Walk through the new connector flow.") + XCTAssertFalse(event.isAllDay) + } + + func testDecodesAllDayEvent() throws { + let events = try decode( + """ + [{ + "event_id": "evt-2", + "title": "Offsite", + "attendees": [], + "attendee_emails": [], + "start_time": "2026-08-26T00:00:00Z", + "end_time": "2026-08-26T23:59:59Z", + "location": "", + "description": "", + "all_day": true + }] + """ + ) + + let event = try XCTUnwrap(events.first).asCalendarEvent + XCTAssertTrue(event.isAllDay) + XCTAssertEqual(event.attendees, []) + XCTAssertEqual(event.location, "") + } +} diff --git a/desktop/macos/Desktop/Tests/UserFacingErrorPresentationTests.swift b/desktop/macos/Desktop/Tests/UserFacingErrorPresentationTests.swift index 4488f7a1fe2..52667384507 100644 --- a/desktop/macos/Desktop/Tests/UserFacingErrorPresentationTests.swift +++ b/desktop/macos/Desktop/Tests/UserFacingErrorPresentationTests.swift @@ -49,6 +49,71 @@ final class UserFacingErrorPresentationTests: XCTestCase { ) } + /// The connector sheet stores an operation's user-facing message as a plain + /// String and re-sanitizes it at display time, so any connector error whose + /// copy the sanitizer rejects reaches the user as "Couldn't connect to + /// . Try again." — the next step the taxonomy exists to give is lost. + /// + /// Driven off the production enums rather than copied literals: editing a + /// message into a shape the sanitizer drops fails here instead of shipping. + func testEveryGoogleConnectorErrorSurvivesDisplaySanitization() { + let calendarErrors: [CalendarReaderError] = [ + .noBrowserFound, + .notSignedIn, + .sessionExpired, + .cookieDecryptionFailed("browser session could not be decrypted"), + .networkError("connection reset"), + .configurationError("API key is invalid or unavailable"), + .pythonNotFound, + ] + let gmailErrors: [GmailReaderError] = [ + .noBrowserFound, + .noGmailCookies, + .notSignedIn, + .sessionExpired, + .cookieDecryptionFailed("browser session could not be decrypted"), + .networkError("connection reset"), + .authFailed, + .pythonNotFound, + ] + + for description in calendarErrors.compactMap(\.errorDescription) { + XCTAssertEqual( + UserFacingErrorPresentation.message(from: description, while: .integration("Calendar")), + description, + "Calendar copy was replaced by the generic fallback" + ) + } + for description in gmailErrors.compactMap(\.errorDescription) { + XCTAssertEqual( + UserFacingErrorPresentation.message(from: description, while: .integration("Gmail")), + description, + "Gmail copy was replaced by the generic fallback" + ) + } + } + + /// The other direction: widening the sanitizer must not start leaking the raw + /// system text it exists to hide. + func testStillHidesRawSystemErrorText() { + let raw = [ + "The operation couldn't be completed. (NSURLErrorDomain error -1009.)", + "The operation couldn't be completed. (NSPOSIXErrorDomain error 2.)", + "Error Domain=kCFErrorDomainCFNetwork Code=310", + "GET https://api.omi.me/v1/dev/user/memories failed", + "upstream returned 503 while reading the response", + "sqlite: prepare: step: no such table: cookies", + ] + + for text in raw { + XCTAssertEqual( + UserFacingErrorPresentation.message(from: text, while: .integration("Calendar")), + "Couldn't connect to Calendar. Try again.", + "raw system text leaked to the user: \(text)" + ) + } + } + func testProvidesNetworkRecovery() { XCTAssertEqual( UserFacingErrorPresentation.message( diff --git a/desktop/macos/changelog/unreleased/20260826-calendar-connect-google-signin.json b/desktop/macos/changelog/unreleased/20260826-calendar-connect-google-signin.json new file mode 100644 index 00000000000..b5ac5a9232b --- /dev/null +++ b/desktop/macos/changelog/unreleased/20260826-calendar-connect-google-signin.json @@ -0,0 +1,3 @@ +{ + "change": "Connecting Calendar now signs you in with Google instead of relying on a Chrome-family browser session, so it works on Safari and Firefox too — and when a connection does fail, Omi tells you what went wrong instead of just \"try again\"" +} diff --git a/desktop/macos/e2e/flows/google-connector-read.yaml b/desktop/macos/e2e/flows/google-connector-read.yaml index 3067b49922f..2d00b83f1c7 100644 --- a/desktop/macos/e2e/flows/google-connector-read.yaml +++ b/desktop/macos/e2e/flows/google-connector-read.yaml @@ -11,6 +11,7 @@ covers: - desktop/macos/Desktop/Sources/BrowserGoogleSession.swift - desktop/macos/Desktop/Sources/GmailReaderService.swift - desktop/macos/Desktop/Sources/CalendarReaderService.swift + - desktop/macos/Desktop/Sources/APIClient+GoogleCalendarGrant.swift - desktop/macos/Desktop/Sources/Integrations/GmailAccountSelection.swift preconditions: - automation_bridge_ready diff --git a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts index 3b1c2be6468..7be7676e1e1 100644 --- a/desktop/windows/src/renderer/src/lib/omiApi.generated.ts +++ b/desktop/windows/src/renderer/src/lib/omiApi.generated.ts @@ -2290,11 +2290,14 @@ export interface GoalUpdate { } export interface GoogleCalendarEvent { + all_day?: boolean; attendee_emails?: Array; attendees?: Array; + description?: string; end_time: string; event_id: string; html_link?: string | null; + location?: string; start_time: string; title: string; } diff --git a/docs/api-reference/app-client-openapi.json b/docs/api-reference/app-client-openapi.json index 5bb7b6dbcf6..5c519f17a9c 100644 --- a/docs/api-reference/app-client-openapi.json +++ b/docs/api-reference/app-client-openapi.json @@ -14369,6 +14369,12 @@ "GoogleCalendarEvent": { "description": "Response model for a Google Calendar event.", "properties": { + "all_day": { + "default": false, + "description": "True when the event has a date but no time", + "title": "All Day", + "type": "boolean" + }, "attendee_emails": { "default": [], "description": "List of attendee email addresses", @@ -14387,6 +14393,12 @@ "title": "Attendees", "type": "array" }, + "description": { + "default": "", + "description": "Event description, truncated for transport", + "title": "Description", + "type": "string" + }, "end_time": { "description": "Event end time", "format": "date-time", @@ -14410,6 +14422,12 @@ "description": "Link to open event in Google Calendar", "title": "Html Link" }, + "location": { + "default": "", + "description": "Event location", + "title": "Location", + "type": "string" + }, "start_time": { "description": "Event start time", "format": "date-time", @@ -34510,7 +34528,7 @@ "schema": { "default": 20, "description": "Maximum number of events to return", - "maximum": 100, + "maximum": 500, "minimum": 1, "title": "Max Results", "type": "integer" diff --git a/web/admin/lib/services/omi-api/omiApi.generated.ts b/web/admin/lib/services/omi-api/omiApi.generated.ts index 3b1c2be6468..7be7676e1e1 100644 --- a/web/admin/lib/services/omi-api/omiApi.generated.ts +++ b/web/admin/lib/services/omi-api/omiApi.generated.ts @@ -2290,11 +2290,14 @@ export interface GoalUpdate { } export interface GoogleCalendarEvent { + all_day?: boolean; attendee_emails?: Array; attendees?: Array; + description?: string; end_time: string; event_id: string; html_link?: string | null; + location?: string; start_time: string; title: string; } diff --git a/web/app/src/lib/omiApi.generated.ts b/web/app/src/lib/omiApi.generated.ts index 3b1c2be6468..7be7676e1e1 100644 --- a/web/app/src/lib/omiApi.generated.ts +++ b/web/app/src/lib/omiApi.generated.ts @@ -2290,11 +2290,14 @@ export interface GoalUpdate { } export interface GoogleCalendarEvent { + all_day?: boolean; attendee_emails?: Array; attendees?: Array; + description?: string; end_time: string; event_id: string; html_link?: string | null; + location?: string; start_time: string; title: string; } diff --git a/web/personas-open-source/src/lib/omiApi.generated.ts b/web/personas-open-source/src/lib/omiApi.generated.ts index 3b1c2be6468..7be7676e1e1 100644 --- a/web/personas-open-source/src/lib/omiApi.generated.ts +++ b/web/personas-open-source/src/lib/omiApi.generated.ts @@ -2290,11 +2290,14 @@ export interface GoalUpdate { } export interface GoogleCalendarEvent { + all_day?: boolean; attendee_emails?: Array; attendees?: Array; + description?: string; end_time: string; event_id: string; html_link?: string | null; + location?: string; start_time: string; title: string; }