fix(caldav): pull Google Calendar events from the events collection, not the /user principal (#2531)

* fix(caldav): pull Google Calendar events from the events collection, not the /user principal

Google serves its CalDAV principal at .../caldav/v2/<id>/user but events live
under .../caldav/v2/<id>/events. The caldav library's principal->home-set
discovery does not reliably enumerate calendars from Google's /user endpoint,
so _sync_blocking fell into its 'treat the URL as a single calendar' fallback
and ran every calendar-query REPORT against the principal URL. /user holds no
VEVENTs, so the REPORT returned a clean but empty 200 for every date range:
auth succeeded, the calendar stayed empty (Apple Calendar works because iCloud
exposes standard discovery at the pasted URL).

Add _google_caldav_events_url() to map a recognised Google principal URL to its
events collection, and route both discovery-less fallbacks through
_open_url_as_calendar() so Google syncs hit /events while other servers' URLs
are used unchanged.

Fixes #2507

* fix(caldav): also map Google's legacy www.google.com/calendar/dav principal URL

Some Google accounts authenticate against the older CalDAV endpoint
(https://www.google.com/calendar/dav/<id>/user) rather than the newer
apidata.googleusercontent.com/caldav/v2 form (reported on #2507). Both have the
same principal-vs-events split, so map the legacy /user URL to its /events
collection as well. The legacy branch is gated on the /calendar/dav/ path so an
unrelated www.google.com URL ending in /user is left untouched.
This commit is contained in:
L1
2026-06-05 10:18:16 -03:00
committed by GitHub
parent 05f047b188
commit 8159733c6c
2 changed files with 210 additions and 2 deletions
+48 -2
View File
@@ -166,6 +166,52 @@ def _find_existing_event(db, pending, uid_val, calendar_id):
).first()
def _google_caldav_events_url(url: str) -> str | None:
"""Map a Google CalDAV *principal* URL to its event-collection URL.
Google serves the principal at ``…/user`` but events live under ``…/events``
— the ``/user`` resource holds no VEVENTs. The `caldav` library's
principal→home-set discovery does not reliably enumerate calendars from
Google's ``/user`` endpoint, so the sync falls into the "treat the URL as a
single calendar" fallback below. Pointed at ``/user`` that fallback issues
every calendar-query REPORT against the principal, which returns a clean but
empty 200 for all date ranges — the calendar shows no events even though
auth succeeded (issue #2507).
Both Google CalDAV endpoint forms are handled, since some accounts only
authenticate against one of them:
- newer: ``https://apidata.googleusercontent.com/caldav/v2/<id>/user``
- legacy: ``https://www.google.com/calendar/dav/<id>/user``
Returns the events URL for a recognised Google principal URL, else None so
the caller keeps the original URL unchanged.
"""
parts = urlparse(url)
host = (parts.hostname or "").lower()
path = parts.path.rstrip("/")
if not path.endswith("/user"):
return None
is_google = (
host.endswith("googleusercontent.com") # newer /caldav/v2 form
or (host in ("www.google.com", "google.com") and "/calendar/dav/" in path) # legacy form
)
if not is_google:
return None
new_path = path[: -len("/user")] + "/events"
return urlunparse(parts._replace(path=new_path))
def _open_url_as_calendar(client, url: str):
"""Open ``url`` as a single calendar collection.
Used when principal discovery yields no calendars. Google's principal URL
is not an event collection, so map it to the events URL first
(see ``_google_caldav_events_url``); other servers' URLs are used as-is.
"""
target = _google_caldav_events_url(url) or url
return client.calendar(url=target)
def _sync_blocking(owner: str, url: str, username: str, password: str) -> dict:
"""The actual sync — synchronous, intended to run in a threadpool.
Returns counts: {calendars, events, deleted, errors}."""
@@ -192,14 +238,14 @@ def _sync_blocking(owner: str, url: str, username: str, password: str) -> dict:
except Exception as e:
logger.info(f"CalDAV principal discovery failed, trying URL as calendar: {e}")
try:
calendars = [client.calendar(url=url)]
calendars = [_open_url_as_calendar(client, url)]
except Exception as e2:
result["errors"].append(f"Could not open URL as calendar: {e2}")
return result
if not calendars:
try:
calendars = [client.calendar(url=url)]
calendars = [_open_url_as_calendar(client, url)]
except Exception as e:
result["errors"].append(f"No calendars and URL fallback failed: {e}")
return result