From 1b7e67dd9760c159e525cd88face00e28a260d13 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 11 Oct 2025 22:54:51 +0200 Subject: [PATCH] fix(calendar): recurring events not working (#4265) --- .../src/nextcloud/nextcloud.integration.ts | 87 +++++++++++-------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index 4e3dbb82e..a1f9a78e2 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -1,4 +1,7 @@ import type { Agent } from "https"; +import dayjs from "dayjs"; +import timezone from "dayjs/plugin/timezone"; +import utc from "dayjs/plugin/utc"; import type { RequestInit as NodeFetchRequestInit } from "node-fetch"; import * as ical from "node-ical"; import { DAVClient } from "tsdav"; @@ -14,6 +17,9 @@ import type { TestingResult } from "../base/test-connection/test-connection-serv import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration"; import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; +dayjs.extend(utc); +dayjs.extend(timezone); + @HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) export class NextcloudIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { @@ -41,47 +47,58 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr ) ).flat(); - return calendarEvents.map((event): CalendarEvent => { - // @ts-expect-error the typescript definitions for this package are wrong - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - const icalData = ical.default.parseICS(event.data) as ical.CalendarResponse; - const veventObject = Object.values(icalData).find((data) => data.type === "VEVENT"); + return calendarEvents + .map((event) => { + // @ts-expect-error the typescript definitions for this package are wrong + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + const icalData = ical.default.parseICS(event.data) as ical.CalendarResponse; + const veventObject = Object.values(icalData).find((data) => data.type === "VEVENT"); - if (!veventObject) { - throw new Error(`Invalid event data object: ${JSON.stringify(event.data)}. Unable to process the calendar.`); - } + if (!veventObject) { + throw new Error(`Invalid event data object: ${JSON.stringify(event.data)}. Unable to process the calendar.`); + } - logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); + logger.debug(`Converting VEVENT event to ${event.etag} from Nextcloud: ${JSON.stringify(veventObject)}`); - const date = veventObject.start; + const eventUrlWithoutHost = new URL(event.url).pathname; + const eventSlug = Buffer.from(eventUrlWithoutHost).toString("base64url"); - const eventUrlWithoutHost = new URL(event.url).pathname; - const dateInMillis = veventObject.start.valueOf(); + const startDates = veventObject.rrule ? veventObject.rrule.between(start, end) : [veventObject.start]; - const url = this.url( - `/apps/calendar/timeGridWeek/now/edit/sidebar/${Buffer.from(eventUrlWithoutHost).toString("base64url")}/${dateInMillis / 1000}`, - ); + const durationMs = veventObject.end.getTime() - veventObject.start.getTime(); - return { - title: veventObject.summary, - subTitle: null, - description: veventObject.description, - startDate: date, - endDate: veventObject.end, - image: null, - location: veventObject.location || null, - indicatorColor: "#ff8600", - links: [ - { - href: url.toString(), - name: "Nextcloud", - logo: "/images/apps/nextcloud.svg", - color: undefined, - isDark: true, - }, - ], - }; - }); + return startDates.map((startDate) => { + const timezoneOffsetMinutes = veventObject.rrule?.origOptions.tzid + ? dayjs(startDate).tz(veventObject.rrule.origOptions.tzid).utcOffset() + : 0; + const utcStartDate = new Date(startDate.getTime() - timezoneOffsetMinutes * 60 * 1000); + const endDate = new Date(utcStartDate.getTime() + durationMs); + const dateInMillis = utcStartDate.valueOf(); + + return { + title: veventObject.summary, + subTitle: null, + description: veventObject.description, + startDate: utcStartDate, + endDate, + image: null, + location: veventObject.location || null, + indicatorColor: "#ff8600", + links: [ + { + href: this.url( + `/apps/calendar/timeGridWeek/now/edit/sidebar/${eventSlug}/${dateInMillis / 1000}`, + ).toString(), + name: "Nextcloud", + logo: "/images/apps/nextcloud.svg", + color: undefined, + isDark: true, + }, + ], + }; + }); + }) + .flat(); } private async createCalendarClientAsync(agent?: Agent) {