feat: Add Traefik Integration with dedicated widget and Add optional Icons for Gotify/Notifcation Widget (ONLY GOTIFY) (#6206)

Co-authored-by: Thomas <thomas@ajnart.fr>
This commit is contained in:
Nerdy TechnicianandThomas authored and GitHub committed 2026-07-10 16:49:24 +02:00
1 parent bcd36b1b27
commit 9864fed775
33 files changed
+998 -46

No files matched your search

@@ -1,12 +1,13 @@
import { ResponseError } from "@homarr/common/server";
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
import { ImageProxy } from "@homarr/image-proxy";
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";
import { gotifyApplicationsResponseSchema, gotifyMessagesResponseSchema } from "./gotify-schema";
export class GotifyIntegration extends Integration implements INotificationsIntegration {
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
@@ -21,23 +22,90 @@ export class GotifyIntegration extends Integration implements INotificationsInte
return { Authorization: `Basic ${credentials}` };
}
private getMessagesUrl(applicationId: number) {
const url = this.externalUrl("/");
url.hash = `/messages/${applicationId}`;
return url.toString();
}
private async getApplicationsByIdAsync(): Promise<Map<number, { name: string; image?: string | null }>> {
try {
const response = await fetchWithTrustedCertificatesAsync(this.url("/application"), {
headers: this.getHeaders(),
});
if (!response.ok) throw new ResponseError(response);
const result = await gotifyApplicationsResponseSchema.safeParseAsync(await response.json());
if (!result.success) return new Map();
return new Map(result.data.map((application) => [application.id, application]));
} catch {
return new Map();
}
}
private async getApplicationIconUrlAsync(
imageProxy: ImageProxy,
imagePath: string | null | undefined,
): Promise<string | undefined> {
const trimmedImagePath = imagePath?.trim();
if (!trimmedImagePath) return undefined;
try {
const imageUrl = this.getApplicationImageUrl(trimmedImagePath);
return await imageProxy.createImageAsync(imageUrl, this.getHeaders());
} catch {
return undefined;
}
}
private getApplicationImageUrl(imagePath: string) {
try {
return new URL(imagePath).toString();
} catch {
const path = imagePath.startsWith("/") ? imagePath : `/${imagePath}`;
return this.url(path as `/${string}`).toString();
}
}
public async getNotificationsAsync(): Promise<Notification[]> {
const url = this.url("/message", { limit: 100 });
const response = await fetchWithTrustedCertificatesAsync(url, { headers: this.getHeaders() });
const [messagesResponse, applicationsById] = await Promise.all([
fetchWithTrustedCertificatesAsync(this.url("/message", { limit: 100 }), { headers: this.getHeaders() }),
this.getApplicationsByIdAsync(),
]);
if (!response.ok) throw new ResponseError(response);
if (!messagesResponse.ok) throw new ResponseError(messagesResponse);
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;
const messagesResult = await gotifyMessagesResponseSchema.safeParseAsync(await messagesResponse.json());
if (!messagesResult.success) throw new Error(`Failed to parse Gotify response: ${messagesResult.error.message}`);
return parsed.messages.map(
(message): Notification => ({
const imageProxy = new ImageProxy();
const iconUrlsByApplicationId = new Map<number, string | undefined>();
await Promise.all(
[...applicationsById.entries()].map(async ([applicationId, application]) => {
iconUrlsByApplicationId.set(
applicationId,
await this.getApplicationIconUrlAsync(imageProxy, application.image),
);
}),
);
return messagesResult.data.messages.map((message): Notification => {
const application = applicationsById.get(message.appid);
return {
id: String(message.id),
time: new Date(message.date),
title: message.title,
body: message.message,
}),
);
href: this.getMessagesUrl(message.appid),
source: application
? {
name: application.name,
iconUrl: iconUrlsByApplicationId.get(message.appid),
}
: undefined,
};
});
}
}
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
// See: https://gotify.net/api-docs#/message/getMessages
export const gotifyMessageSchema = z.object({
id: z.number(),
appid: z.number(),
date: z.string(),
title: z.string(),
message: z.string(),
@@ -12,3 +13,11 @@ export const gotifyMessageSchema = z.object({
export const gotifyMessagesResponseSchema = z.object({
messages: z.array(gotifyMessageSchema),
});
export const gotifyApplicationSchema = z.object({
id: z.number(),
name: z.string(),
image: z.string().nullable().optional(),
});
export const gotifyApplicationsResponseSchema = z.array(gotifyApplicationSchema);