fix(import): read the Fossify/Simple Calendar .ics dialect (#225)
Their exporter writes the last day an all-day event occupies as DTEND, where RFC 5545 means the day after. Read literally every all-day event was a day short and single-day ones — birthdays, memorials, name days — came out zero length, which the provider expands into no instances at all, so they never appeared. IcsQuirks sniffs the producer and shifts the end; independently, an all-day event may no longer end at or before its start and an all-day DURATION is floored at P1D. Fossify also emits a bare ";BYDAY=" for weekly events with no weekday mask. EventRecurrence.parse throws on that, out of insert, and nothing caught it — one such row failed the entire file. sanitizeRrule now drops empty and malformed parts, and importEvents isolates each event, counting rejects into IcsImportSummary.failed instead of unwinding the batch. Rest of the dialect: VTODO imports as events (was dropped silently), CATEGORIES stands in for the X-WR-CALNAME they never write, bare EXDATE day codes resolve against a timed series' own time of day, and their positive VALARM trigger is read as "on the day" so all-day reminders fire at the hour the setting names rather than at UTC midnight. Colours come across too: X-FOSSIFY-EVENT-COLOR / COLOR / the category colour as fallback, plus the X-SMT-* legacy spellings, snapped in Oklab to the nearest key a palette account publishes since those reject a raw EVENT_COLOR. Closes #225
This commit is contained in:
@@ -346,6 +346,48 @@ a latch that disables its own scheduling once a real broadcast arrives — canno
|
||||
be copied, because our failure mode includes a broadcast that arrives with no
|
||||
alert row behind it.
|
||||
|
||||
## Importing foreign `.ics`
|
||||
|
||||
`IcsParser` is deliberately liberal, because the files it is handed were written
|
||||
by other people's calendars. Two lessons from migrating users off Fossify
|
||||
Calendar (#225) shape the read path.
|
||||
|
||||
**All-day `DTEND` is not always exclusive.** RFC 5545 defines it as the first
|
||||
day *after* the event; the Simple Calendar / Fossify family writes the last day
|
||||
the event occupies. Read literally, every one of their all-day events is a day
|
||||
short and single-day ones — birthdays, anniversaries, name days, i.e. most of
|
||||
what anyone migrates — collapse to zero length, at which point the provider
|
||||
expands the series into no instances and the event simply is not there.
|
||||
`IcsQuirks` sniffs the producer (`PRODID`, or the `X-FOSSIFY-*` / `X-SMT-*`
|
||||
extensions for files that passed through a rewriter) and shifts the end by a
|
||||
day. Independently of any dialect, an all-day event is never allowed to end at
|
||||
or before it starts, and `buildImportedEventValues` floors an all-day `DURATION`
|
||||
at `P1D` — belt and braces, because the failure is invisible rather than loud.
|
||||
|
||||
**One bad event must not cost the file.** The provider validates `RRULE` through
|
||||
`EventRecurrence.parse`, which *throws* — out of `insert`, not out of a later
|
||||
read. Fossify emits a bare `;BYDAY=` for any weekly event with no weekday mask,
|
||||
so a single such row used to abort the whole import with nothing but "failed" on
|
||||
screen. `sanitizeRrule` drops empty and malformed parts before the write, and
|
||||
`CalendarRepositoryImpl.importEvents` isolates each event, counting rejects into
|
||||
`IcsImportSummary.failed` rather than unwinding the batch.
|
||||
|
||||
The rest of the dialect handling is smaller: `VTODO` components import as events
|
||||
(Calendula models no tasks; dropping them silently lost half of some exports),
|
||||
`CATEGORIES` stands in for the `X-WR-CALNAME` Fossify never writes, bare `EXDATE`
|
||||
day codes on a timed series are resolved against the series' own time of day, and
|
||||
a positive `VALARM` trigger on an all-day event is that family's encoding of "on
|
||||
the day at 09:00" — read as zero days before, so it fires at the hour the user's
|
||||
own setting names.
|
||||
|
||||
Colour arrives as a raw ARGB from an app with no idea which account it is landing
|
||||
in. A calendar whose account publishes a palette rejects a raw `EVENT_COLOR`, so
|
||||
`buildImportedEventValues` snaps the imported colour to the nearest published key
|
||||
(`nearestTo`, measured in Oklab) and writes the raw value only where there is no
|
||||
palette. The single-event review form deliberately keeps no colour at all: its
|
||||
first question is which calendar to use, and answering it clears the colour
|
||||
anyway.
|
||||
|
||||
## Testing
|
||||
|
||||
JUnit 5 + Truth + Turbine on the JVM. The seams that make it work:
|
||||
|
||||
Reference in New Issue
Block a user