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 <lorinc1231@gmail.com>
Co-authored-by: Lorinc936 <116025544+Lorinc936@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-03-27 18:51:43 +01:00
1 parent 22bbef9d8e
commit f88bf9582e
5 files changed
+51 -7

No files matched your search

+1 -1
View File
@@ -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: {
@@ -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<TestingResult> {
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<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,
@@ -57,10 +57,12 @@ export const AddBoardModal = createModal(({ actions }) => {
{...form.getInputProps("name")}
description={
boardNameStatus.description ? (
<Group c={boardNameStatus.description.color} gap="xs" align="center">
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
<span>{boardNameStatus.description.label}</span>
</Group>
<span>
<Group c={boardNameStatus.description.color} gap="xs" align="center">
{boardNameStatus.description.icon ? <boardNameStatus.description.icon size={16} /> : null}
<span>{boardNameStatus.description.label}</span>
</Group>
</span>
) : null
}
/>
@@ -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,
@@ -119,4 +119,5 @@ const permissionMapping = {
allowGeolocation: "geolocation",
allowMicrophone: "microphone",
allowPayment: "payment",
allowModals: "allow-modals",
} satisfies Record<keyof Omit<WidgetComponentProps<"iframe">["options"], "embedUrl" | "allowScrolling">, string>;