From f88bf9582e1cf45a6b7fd898e12d227869a3849f Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:51:43 +0100 Subject: [PATCH] feat: add nextcloud notification provider (#5343) * 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 Co-authored-by: Lorinc936 <116025544+Lorinc936@users.noreply.github.com> --- packages/definitions/src/integration.ts | 2 +- .../src/nextcloud/nextcloud.integration.ts | 44 ++++++++++++++++++- .../src/boards/add-board-modal.tsx | 10 +++-- packages/old-import/src/widgets/options.ts | 1 + packages/widgets/src/iframe/component.tsx | 1 + 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index bbcfcde5f..06f6aa73f 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -217,7 +217,7 @@ export const integrationDefs = { name: "Nextcloud", secretKinds: [["username", "password"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nextcloud.svg", - category: ["calendar"], + category: ["calendar", "notifications"], documentationUrl: createDocumentationLink("/docs/integrations/nextcloud"), }, unifiController: { diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index aa57f480f..531191cf6 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -5,8 +5,10 @@ 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 { createHttpsAgentAsync } from "@homarr/core/infrastructure/http"; +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"; @@ -16,6 +18,21 @@ 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" }); @@ -23,7 +40,7 @@ dayjs.extend(utc); dayjs.extend(timezone); @HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) -export class NextcloudIntegration extends Integration implements ICalendarIntegration { +export class NextcloudIntegration extends Integration implements ICalendarIntegration, INotificationsIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const client = await this.createCalendarClientAsync(await createHttpsAgentAsync(input.options)); await client.login(); @@ -104,6 +121,29 @@ export class NextcloudIntegration extends Integration implements ICalendarIntegr .flat(); } + public async getNotificationsAsync(): Promise { + 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, diff --git a/packages/modals-collection/src/boards/add-board-modal.tsx b/packages/modals-collection/src/boards/add-board-modal.tsx index 919335b9b..b18ca9851 100644 --- a/packages/modals-collection/src/boards/add-board-modal.tsx +++ b/packages/modals-collection/src/boards/add-board-modal.tsx @@ -57,10 +57,12 @@ export const AddBoardModal = createModal(({ actions }) => { {...form.getInputProps("name")} description={ boardNameStatus.description ? ( - - {boardNameStatus.description.icon ? : null} - {boardNameStatus.description.label} - + + + {boardNameStatus.description.icon ? : null} + {boardNameStatus.description.label} + + ) : null } /> diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index e3c58768f..194d21d21 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -101,6 +101,7 @@ const optionMapping: OptionMapping = { allowMicrophone: (oldOptions) => oldOptions.allowMicrophone, allowGeolocation: (oldOptions) => oldOptions.allowGeolocation, allowScrolling: (oldOptions) => oldOptions.allowScrolling, + allowModals: () => undefined, }, video: { feedUrl: (oldOptions) => oldOptions.FeedUrl, diff --git a/packages/widgets/src/iframe/component.tsx b/packages/widgets/src/iframe/component.tsx index 74032c5f5..ac794a631 100644 --- a/packages/widgets/src/iframe/component.tsx +++ b/packages/widgets/src/iframe/component.tsx @@ -119,4 +119,5 @@ const permissionMapping = { allowGeolocation: "geolocation", allowMicrophone: "microphone", allowPayment: "payment", + allowModals: "allow-modals", } satisfies Record["options"], "embedUrl" | "allowScrolling">, string>;