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