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";