fix(import): make .ics import survive events the provider rejects (#225)
importEvents inserted every event in one unguarded loop, so a single row the provider refused — it throws on a malformed RRULE straight out of insert — aborted the batch and left the user with "couldn't read this file" and nothing imported. Each event is now isolated and rejects are counted into IcsImportSummary.failed and shown. sanitizeRrule drops empty and malformed rule parts before the write. Alongside that, several things a foreign file carries that we dropped: VTODO components (silently lost, now imported as events), EXDATE, the CATEGORIES calendar name, and colours — X-FOSSIFY-EVENT-COLOR / COLOR / the category colour plus the X-SMT-* legacy spellings, snapped in Oklab to the nearest key a palette account publishes since those reject a raw EVENT_COLOR. Two correctness fixes on our side: an all-day event may no longer end at or before it starts (the provider expands a zero-length series into no instances, so it just disappears), and imported all-day reminders now go through the same encoding as hand-created ones instead of firing at UTC midnight. A VALARM trigger pointing after the start is read as a time of day rather than clamped to zero. Note for later: their all-day DTEND is RFC-correct — endTS anchors at noon of the last day and the exporter's +12h rounds it to the following midnight. Do not "fix" it by sniffing PRODID; the holiday files bundled in Fossify are conformant and name Fossify in theirs. One is kept as a fixture. Refs #225
This commit is contained in:
@@ -346,6 +346,53 @@ 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. What that costs us, learned from reading the Simple
|
||||
Calendar / Fossify exporter (#225):
|
||||
|
||||
**An all-day event may never reach the provider zero days long.** RFC 5545
|
||||
§3.6.1 gives a `DATE`-valued `DTSTART` with no `DTEND` a one-day duration, and a
|
||||
`DTEND` equal to `DTSTART` is meaningless — but the failure is silent rather than
|
||||
loud: the provider expands a zero-length series into no instances at all, so the
|
||||
event is simply not there, with nothing logged. `resolveEnd` floors the span at a
|
||||
day and `buildImportedEventValues` floors an all-day `DURATION` at `P1D`.
|
||||
|
||||
Resist the urge to correct a producer's all-day `DTEND` by sniffing its `PRODID`.
|
||||
Fossify's looks like it needs it and does not: `Event.endTS` anchors an all-day
|
||||
event at *noon* of its last day (`EventActivity.getStartEndTimes`), or at the
|
||||
exclusive midnight when the row came from CalDAV, and the exporter's
|
||||
`+ TWELVE_HOURS` rounds either to the following midnight — correct on both paths.
|
||||
Shifting it would push every imported all-day event out by a day. The holiday
|
||||
files bundled inside Fossify are a second trap: their `PRODID` reads
|
||||
`Fossify Calendar Holiday Generator` and their content is plain conformant
|
||||
iCalendar. One is kept as a fixture in `app/src/test/resources/ics`.
|
||||
|
||||
**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. `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 and reporting only
|
||||
"couldn't read this file".
|
||||
|
||||
Smaller dialect handling: `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 `VALARM`
|
||||
trigger pointing *after* the start — which is how that family encodes "on the day
|
||||
at 09:00" — is read as zero days before rather than clamped to a lead time of
|
||||
zero, so all-day reminders fire at the hour the user's own setting names instead
|
||||
of at UTC midnight.
|
||||
|
||||
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