Files
Homarr/packages/integrations/src/nextcloud/nextcloud.integration.ts
Nicolas Newman fedbff3fd1 feat(integrations): add ICal (#3980)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
2025-09-17 18:57:50 +02:00

104 lines
4.0 KiB
TypeScript

import type { Agent } from "https";
import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
import * as ical from "node-ical";
import { DAVClient } from "tsdav";
import { createHttpsAgentAsync } from "@homarr/certificates/server";
import { logger } from "@homarr/log";
import { HandleIntegrationErrors } from "../base/errors/decorator";
import { integrationTsdavHttpErrorHandler } from "../base/errors/http";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
export class NextcloudIntegration extends Integration implements ICalendarIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options));
await client.login();
return { success: true };
}
public async getCalendarEventsAsync(start: Date, end: Date, _showUnmonitored?: boolean): Promise<CalendarEvent[]> {
const client = await this.createCalendarClientAsync();
await client.login();
const calendars = await client.fetchCalendars();
// Parameters must be in ISO-8601, See https://tsdav.vercel.app/docs/caldav/fetchCalendarObjects#arguments
const calendarEvents = (
await Promise.all(
calendars.map(
async (calendar) =>
await client.fetchCalendarObjects({
calendar,
timeRange: { start: start.toISOString(), end: end.toISOString() },
}),
),
)
).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");
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)}`);
const date = veventObject.start;
const eventUrlWithoutHost = new URL(event.url).pathname;
const dateInMillis = veventObject.start.valueOf();
const url = this.url(
`/apps/calendar/timeGridWeek/now/edit/sidebar/${Buffer.from(eventUrlWithoutHost).toString("base64url")}/${dateInMillis / 1000}`,
);
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,
},
],
};
});
}
private async createCalendarClientAsync(agent?: Agent) {
return new DAVClient({
serverUrl: this.integration.url,
credentials: {
username: this.getSecretValue("username"),
password: this.getSecretValue("password"),
},
authMethod: "Basic",
defaultAccountType: "caldav",
fetchOptions: {
// tsdav is using cross-fetch which uses node-fetch for nodejs environments.
// There is an agent property that is the same type as the http(s) agents of nodejs
agent: agent ?? (await createHttpsAgentAsync()),
} satisfies NodeFetchRequestInit as RequestInit,
});
}
}