feat(integrations): add Gotify notifications integration (#5847)

* 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.
This commit is contained in:
Nerdy Technician authored and GitHub committed 2026-06-16 10:29:48 +02:00
1 parent a4f0eda4a0
commit 5a1967bdd4
6 files changed
+66

No files matched your search

@@ -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"
+7
View File
@@ -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"]],
@@ -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,
@@ -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<TestingResult> {
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<Notification[]> {
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,
}));
}
}
@@ -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),
});
+1
View File
@@ -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";