* feat: add Nextcloud notification provider fix: resolve hydration error in add-board modal * Update packages/integrations/src/nextcloud/nextcloud.integration.ts Co-authored-by: Manuel <30572287+manuel-rw@users.noreply.github.com> * Update nextcloud.integration.ts * defined notificationSchema outside of class --------- Co-authored-by: Lorinc936 <lorinc1231@gmail.com> Co-authored-by: Lorinc936 <116025544+Lorinc936@users.noreply.github.com>
164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
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";
|
|
import { z } from "zod";
|
|
|
|
import { ResponseError } from "@homarr/common/server";
|
|
import { createHttpsAgentAsync, fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
|
|
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";
|
|
import type { Notification } from "../interfaces/notifications/notification-types";
|
|
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
|
|
|
const notificationSchema = z.object({
|
|
ocs: z.object({
|
|
data: z.array(
|
|
z.object({
|
|
notification_id: z.number(),
|
|
datetime: z.string(),
|
|
subject: z.string(),
|
|
message: z.string(),
|
|
}),
|
|
),
|
|
}),
|
|
});
|
|
|
|
const logger = createLogger({ module: "nextcloudIntegration" });
|
|
|
|
dayjs.extend(utc);
|
|
dayjs.extend(timezone);
|
|
|
|
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
|
|
export class NextcloudIntegration extends Integration implements ICalendarIntegration, INotificationsIntegration {
|
|
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) => {
|
|
// @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 eventUrlWithoutHost = new URL(event.url).pathname;
|
|
const eventSlug = Buffer.from(eventUrlWithoutHost).toString("base64url");
|
|
|
|
const startDates = veventObject.rrule ? veventObject.rrule.between(start, end) : [veventObject.start];
|
|
|
|
const durationMs = veventObject.end.getTime() - veventObject.start.getTime();
|
|
|
|
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:
|
|
"color" in veventObject && typeof veventObject.color === "string" ? veventObject.color : "#ff8600",
|
|
links: [
|
|
{
|
|
href: this.externalUrl(
|
|
`/apps/calendar/timeGridWeek/now/edit/sidebar/${eventSlug}/${dateInMillis / 1000}`,
|
|
).toString(),
|
|
name: "Nextcloud",
|
|
logo: "/images/apps/nextcloud.svg",
|
|
color: undefined,
|
|
isDark: true,
|
|
},
|
|
],
|
|
};
|
|
});
|
|
})
|
|
.flat();
|
|
}
|
|
|
|
public async getNotificationsAsync(): Promise<Notification[]> {
|
|
const url = this.url("/ocs/v2.php/apps/notifications/api/v2/notifications", { format: "json" });
|
|
const response = await fetchWithTrustedCertificatesAsync(url, {
|
|
headers: {
|
|
Authorization: `Basic ${Buffer.from(
|
|
`${this.getSecretValue("username")}:${this.getSecretValue("password")}`,
|
|
).toString("base64")}`,
|
|
"OCS-APIRequest": "true",
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new ResponseError(response);
|
|
|
|
const json = notificationSchema.parse(await response.json());
|
|
|
|
return json.ocs.data.map((notification) => ({
|
|
id: String(notification.notification_id),
|
|
time: new Date(notification.datetime),
|
|
title: notification.subject,
|
|
body: notification.message,
|
|
}));
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
}
|