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:
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"
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
Reference in new issue
Block a user