From 5a1967bdd4c1af2b30ff2ddc005516e70d16c1ee Mon Sep 17 00:00:00 2001 From: Nerdy Technician Date: Tue, 16 Jun 2026 09:29:48 +0100 Subject: [PATCH] feat(integrations): add Gotify notifications integration (#5847) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(integrations): add Gotify notifications integration Closes #4186 > "It would be really useful if Homarr could integrate with Gotify — > view notifications from Gotify right inside the Homarr dashboard, > maybe as a widget or a small panel. A widget that displays the latest > messages from Gotify (with title, message, time)." > — #4186: feat: Gotify Integration - display notifications in Homarr - `gotify/gotify-schema.ts` — Zod schema validating the `GET /message` API response from the Gotify server, including id, date, title, message body, and optional priority fields. - `gotify/gotify-integration.ts` — `GotifyIntegration` class extending the base `Integration` and implementing `INotificationsIntegration`. Authenticates via HTTP Basic Auth (username + password) and fetches the latest 100 messages from the `/message` endpoint. Connection testing hits `/health`. - `index.ts` — exports `GotifyIntegration` from the package public API. - `base/creator.ts` — registers `GotifyIntegration` in the integration creator map so it can be instantiated at runtime. - `integration.ts` — registers the `gotify` integration definition with: - `secretKinds: [["username", "password"]]` (Basic Auth credentials) - `category: ["notifications"]` (picked up automatically by the Notifications widget via `getIntegrationKindsByCategory`) - Icon sourced from `homarr-labs/dashboard-icons` - Documentation URL pointing to `/docs/integrations/gotify` - `docs/homarr-docs-sitemap.ts` — adds `/docs/integrations/gotify` to the `HomarrDocumentationPath` union type so the documentation link passes the TypeScript path validation check. Because `gotify` is registered under the `notifications` category, the existing Notifications widget automatically supports it with no widget code changes required. Users add the integration via Settings → Integrations, supply their Gotify server URL, username, and password, then attach it to a Notifications widget on any board. * refactor(integrations): inline json parse and add error handling in Gotify integration Address PR review feedback: remove intermediate `as unknown` cast by inlining response.json() directly, and use safeParseAsync to handle deserialization failures gracefully instead of throwing unhandled. --- .../src/docs/homarr-docs-sitemap.ts | 1 + packages/definitions/src/integration.ts | 7 ++++ packages/integrations/src/base/creator.ts | 2 + .../src/gotify/gotify-integration.ts | 41 +++++++++++++++++++ .../integrations/src/gotify/gotify-schema.ts | 14 +++++++ packages/integrations/src/index.ts | 1 + 6 files changed, 66 insertions(+) create mode 100644 packages/integrations/src/gotify/gotify-integration.ts create mode 100644 packages/integrations/src/gotify/gotify-schema.ts diff --git a/packages/definitions/src/docs/homarr-docs-sitemap.ts b/packages/definitions/src/docs/homarr-docs-sitemap.ts index 39ee81c08..158cc58b9 100644 --- a/packages/definitions/src/docs/homarr-docs-sitemap.ts +++ b/packages/definitions/src/docs/homarr-docs-sitemap.ts @@ -178,6 +178,7 @@ export type HomarrDocumentationPath = | "/docs/integrations/emby" | "/docs/integrations/github-containerregistry" | "/docs/integrations/github" + | "/docs/integrations/gotify" | "/docs/integrations/gitlab" | "/docs/integrations/glances" | "/docs/integrations/home-assistant" diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 5a91bef72..195fc81c5 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -355,6 +355,13 @@ export const integrationDefs = { documentationUrl: createDocumentationLink("/docs/integrations/ntfy"), defaultPort: 80, }, + gotify: { + name: "Gotify", + secretKinds: [["username", "password"]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/gotify.svg", + category: ["notifications"], + documentationUrl: createDocumentationLink("/docs/integrations/gotify"), + }, ical: { name: "iCal", secretKinds: [["url"]], diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index f2d9547fc..ca0595651 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -35,6 +35,7 @@ import { MockIntegration } from "../mock/mock-integration"; import { NavidromeIntegration } from "../navidrome/navidrome-integration"; import { NextcloudIntegration } from "../nextcloud/nextcloud.integration"; import { NPMIntegration } from "../npm/npm-integration"; +import { GotifyIntegration } from "../gotify/gotify-integration"; import { NTFYIntegration } from "../ntfy/ntfy-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; import { OPNsenseIntegration } from "../opnsense/opnsense-integration"; @@ -121,6 +122,7 @@ export const integrationCreators = { ical: ICalIntegration, quay: QuayIntegration, ntfy: NTFYIntegration, + gotify: GotifyIntegration, mock: MockIntegration, truenas: TrueNasIntegration, unraid: UnraidIntegration, diff --git a/packages/integrations/src/gotify/gotify-integration.ts b/packages/integrations/src/gotify/gotify-integration.ts new file mode 100644 index 000000000..b7fd9361e --- /dev/null +++ b/packages/integrations/src/gotify/gotify-integration.ts @@ -0,0 +1,41 @@ +import { ResponseError } from "@homarr/common/server"; +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +import { Integration } from "../base/integration"; +import type { IntegrationTestingInput } from "../base/integration"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { Notification } from "../interfaces/notifications/notification-types"; +import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration"; +import { gotifyMessagesResponseSchema } from "./gotify-schema"; + +export class GotifyIntegration extends Integration implements INotificationsIntegration { + public async testingAsync(input: IntegrationTestingInput): Promise { + await input.fetchAsync(this.url("/health"), { headers: this.getHeaders() }); + return { success: true }; + } + + private getHeaders() { + const credentials = Buffer.from( + `${super.getSecretValue("username")}:${super.getSecretValue("password")}`, + ).toString("base64"); + return { Authorization: `Basic ${credentials}` }; + } + + public async getNotificationsAsync(): Promise { + const url = this.url("/message", { limit: 100 }); + const response = await fetchWithTrustedCertificatesAsync(url, { headers: this.getHeaders() }); + + if (!response.ok) throw new ResponseError(response); + + const result = await gotifyMessagesResponseSchema.safeParseAsync(await response.json()); + if (!result.success) throw new Error(`Failed to parse Gotify response: ${result.error.message}`); + const parsed = result.data; + + return parsed.messages.map((message): Notification => ({ + id: String(message.id), + time: new Date(message.date), + title: message.title, + body: message.message, + })); + } +} diff --git a/packages/integrations/src/gotify/gotify-schema.ts b/packages/integrations/src/gotify/gotify-schema.ts new file mode 100644 index 000000000..5c89774ec --- /dev/null +++ b/packages/integrations/src/gotify/gotify-schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod/v4"; + +// See: https://gotify.net/api-docs#/message/getMessages +export const gotifyMessageSchema = z.object({ + id: z.number(), + date: z.string(), + title: z.string(), + message: z.string(), + priority: z.number().optional(), +}); + +export const gotifyMessagesResponseSchema = z.object({ + messages: z.array(gotifyMessageSchema), +}); diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index 34c994ad3..64d8776bc 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -17,6 +17,7 @@ export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; export { ReadarrIntegration } from "./media-organizer/readarr/readarr-integration"; export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { NextcloudIntegration } from "./nextcloud/nextcloud.integration"; +export { GotifyIntegration } from "./gotify/gotify-integration"; export { NTFYIntegration } from "./ntfy/ntfy-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { GlancesIntegration } from "./glances/glances-integration";