Add two-way CalDAV sync for tasks (#811) - #2280
schobernoise wants to merge 1 commit into
Conversation
Keeps a vault in sync with a CalDAV task list (Nextcloud, Apple Reminders, Radicale, Baikal) in both directions, as VTODOs rather than calendar events. This is a new integration alongside the Google/Microsoft export, not an extension of it: that path is one-way and models tasks as VEVENTs. Identity lives in each note's frontmatter (caldav_uid, caldav_href, caldav_etag, caldav_account, caldav_synced_at), so disconnecting and reconnecting converges instead of duplicating. ETags detect a concurrent edit; DTSTAMP/LAST-MODIFIED against the local change time decides it. Most of the logic is in pure modules under src/services/caldav/, registered in run-architecture-conformance.mjs so they are held to the stricter rules. VTODOs are modelled as content lines rather than a decoded struct, so properties TaskNotes does not map -- DESCRIPTION, ATTACH, X- properties -- survive a round trip untouched. Subtasks map to RELATED-TO;RELTYPE=PARENT. TaskNotes has no parent field: a subtask is a task whose projects field points at its parent, and both sides are many-valued, so the mapping is direct. Dependencies use the RFC 9253 temporal reltypes with GAP, which TaskDependency already matches field for field. A relation whose target has no UID yet is replayed once after the rest of the pass, and one that genuinely cannot be addressed is left out rather than guessed at. Reminders map to VALARM. TaskNotes stamps its own alarms with X-TASKNOTES-REMINDER and rewrites only those, so an alarm added on a phone is never touched. Polling asks for the collection ctag first and stops there when nothing has changed; otherwise a VTODO-filtered calendar-query returns only tasks, and returns all of them, which is what makes deletion detection safe. Task lists commonly share a collection with far more events -- one test list held 1142 resources and a single VTODO -- so walking the resource list would mean downloading the entire calendar to find nothing. Discovery climbs from the configured URL up to the account root on the original scheme. /.well-known/caldav is tried last: it frequently redirects to plain http behind a reverse proxy, and requestUrl follows redirects with no way to veto a scheme downgrade. Credentials live in Obsidian's SecretStorage, never in data.json, and are refused over anything but HTTPS except to loopback. Claude-Session: https://claude.ai/code/session_01Q8Ni1X5i6HsZaEAueYLvZd
|
Thanks again, @schobernoise , both for the implementation and for being open to the companion-plugin approach. I owe you a clarification following my earlier comment about aiming to support this in v5. Having thought more carefully about the ongoing maintenance, I realise I was too optimistic about what I could take on. I don’t use CalDAV myself, and I wouldn’t be able to give a bidirectional integration the regular testing and attention it needs. I’m sorry for having raised that expectation. My preference remains for this to live in a companion plugin, but it would need someone other than me to own and maintain it. TaskNotes already has a runtime API for companion plugins, which should cover much of what this needs. Would maintaining it independently be something you’d be interested in? I’d be happy to link to it from the TaskNotes documentation. I also completely understand if that’s more than you intended to take on when submitting this PR. I’m very grateful for the work you and @slipstyle have put into this and I’m sorry that I can’t follow through in the way my earlier reply may have suggested. |
Keeps a vault in sync with a CalDAV task list (Nextcloud, Apple Reminders, Radicale, Baikal) in both directions, as VTODOs rather than calendar events. This is a new integration alongside the Google/Microsoft export, not an extension of it: that path is one-way and models tasks as VEVENTs.
Identity lives in each note's frontmatter (caldav_uid, caldav_href, caldav_etag, caldav_account, caldav_synced_at), so disconnecting and reconnecting converges instead of duplicating. ETags detect a concurrent edit; DTSTAMP/LAST-MODIFIED against the local change time decides it.
Most of the logic is in pure modules under src/services/caldav/, registered in run-architecture-conformance.mjs so they are held to the stricter rules. VTODOs are modelled as content lines rather than a decoded struct, so properties TaskNotes does not map -- DESCRIPTION, ATTACH, X- properties -- survive a round trip untouched.
Subtasks map to RELATED-TO;RELTYPE=PARENT. TaskNotes has no parent field: a subtask is a task whose projects field points at its parent, and both sides are many-valued, so the mapping is direct. Dependencies use the RFC 9253 temporal reltypes with GAP, which TaskDependency already matches field for field. A relation whose target has no UID yet is replayed once after the rest of the pass, and one that genuinely cannot be addressed is left out rather than guessed at.
Reminders map to VALARM. TaskNotes stamps its own alarms with X-TASKNOTES-REMINDER and rewrites only those, so an alarm added on a phone is never touched.
Polling asks for the collection ctag first and stops there when nothing has changed; otherwise a VTODO-filtered calendar-query returns only tasks, and returns all of them, which is what makes deletion detection safe. Task lists commonly share a collection with far more events -- one test list held 1142 resources and a single VTODO -- so walking the resource list would mean downloading the entire calendar to find nothing.
Discovery climbs from the configured URL up to the account root on the original scheme. /.well-known/caldav is tried last: it frequently redirects to plain http behind a reverse proxy, and requestUrl follows redirects with no way to veto a scheme downgrade.
Credentials live in Obsidian's SecretStorage, never in data.json, and are refused over anything but HTTPS except to loopback.
Claude-Session: https://claude.ai/code/session_01Q8Ni1X5i6HsZaEAueYLvZd