diff --git a/packages/api/src/router/integration/integration-router.ts b/packages/api/src/router/integration/integration-router.ts index 8fa570f8d..e133e2b9c 100644 --- a/packages/api/src/router/integration/integration-router.ts +++ b/packages/api/src/router/integration/integration-router.ts @@ -23,7 +23,7 @@ import { integrationKinds, integrationSecretKindObject, } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { validation } from "@homarr/validation"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -465,7 +465,7 @@ export const integrationRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search"))) .input(z.object({ integrationId: z.string(), query: z.string() })) .query(async ({ ctx, input }) => { - const integrationInstance = integrationCreator(ctx.integration); + const integrationInstance = await createIntegrationAsync(ctx.integration); return await integrationInstance.searchAsync(encodeURI(input.query)); }), }); diff --git a/packages/api/src/router/integration/integration-test-connection.ts b/packages/api/src/router/integration/integration-test-connection.ts index db36ef568..96ae9f4f4 100644 --- a/packages/api/src/router/integration/integration-test-connection.ts +++ b/packages/api/src/router/integration/integration-test-connection.ts @@ -4,7 +4,7 @@ import { decryptSecret } from "@homarr/common/server"; import type { Integration } from "@homarr/db/schema"; import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions"; import { getAllSecretKindOptions } from "@homarr/definitions"; -import { integrationCreator, IntegrationTestConnectionError } from "@homarr/integrations"; +import { createIntegrationAsync, IntegrationTestConnectionError } from "@homarr/integrations"; import { logger } from "@homarr/log"; type FormIntegration = Integration & { @@ -66,7 +66,7 @@ export const testConnectionAsync = async ( const { secrets: _, ...baseIntegration } = integration; - const integrationInstance = integrationCreator({ + const integrationInstance = await createIntegrationAsync({ ...baseIntegration, decryptedSecrets, }); diff --git a/packages/api/src/router/search-engine/search-engine-router.ts b/packages/api/src/router/search-engine/search-engine-router.ts index aebaf63a3..7b6eee219 100644 --- a/packages/api/src/router/search-engine/search-engine-router.ts +++ b/packages/api/src/router/search-engine/search-engine-router.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { asc, createId, eq, like } from "@homarr/db"; import { getServerSettingByKeyAsync } from "@homarr/db/queries"; import { searchEngines, users } from "@homarr/db/schema"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { validation } from "@homarr/validation"; import { createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -134,14 +134,14 @@ export const searchEngineRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr")) .input(validation.common.mediaRequestOptions) .query(async ({ ctx, input }) => { - const integration = integrationCreator(ctx.integration); + const integration = await createIntegrationAsync(ctx.integration); return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId); }), requestMedia: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr")) .input(validation.common.requestMedia) .mutation(async ({ ctx, input }) => { - const integration = integrationCreator(ctx.integration); + const integration = await createIntegrationAsync(ctx.integration); return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons); }), create: permissionRequiredProcedure diff --git a/packages/api/src/router/test/integration/integration-test-connection.spec.ts b/packages/api/src/router/test/integration/integration-test-connection.spec.ts index e692543b6..fad9e961e 100644 --- a/packages/api/src/router/test/integration/integration-test-connection.spec.ts +++ b/packages/api/src/router/test/integration/integration-test-connection.spec.ts @@ -18,11 +18,13 @@ vi.mock("@homarr/common/server", async (importActual) => { describe("testConnectionAsync should run test connection of integration", () => { test("with input of only form secrets matching api key kind it should use form apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -58,11 +60,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -105,11 +109,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of form and db secrets matching api key kind it should use form apiKey", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["apiKey"]]); const integration = { @@ -152,11 +158,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); const integration = { @@ -203,11 +211,13 @@ describe("testConnectionAsync should run test connection of integration", () => test("with input of null form apiKey and db secrets for username and password it should use db username and password when both is allowed", async () => { // Arrange - const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator"); + const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync"); const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions"); - factorySpy.mockReturnValue({ - testConnectionAsync: async () => await Promise.resolve(), - } as homarrIntegrations.PiHoleIntegration); + factorySpy.mockReturnValue( + Promise.resolve({ + testConnectionAsync: async () => await Promise.resolve(), + } as homarrIntegrations.PiHoleIntegrationV6), + ); optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]); const integration = { diff --git a/packages/api/src/router/widgets/dns-hole.ts b/packages/api/src/router/widgets/dns-hole.ts index 603fe992f..38c016913 100644 --- a/packages/api/src/router/widgets/dns-hole.ts +++ b/packages/api/src/router/widgets/dns-hole.ts @@ -1,12 +1,12 @@ import { observable } from "@trpc/server/observable"; +import { z } from "zod"; import type { Modify } from "@homarr/common/types"; import type { Integration } from "@homarr/db/schema"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; -import { controlsInputSchema } from "@homarr/integrations/types"; import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole"; import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration"; @@ -65,7 +65,7 @@ export const dnsHoleRouter = createTRPCRouter({ enable: protectedProcedure .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .mutation(async ({ ctx: { integration } }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.enableAsync(); const innerHandler = dnsHoleRequestHandler.handler(integration, {}); @@ -76,10 +76,14 @@ export const dnsHoleRouter = createTRPCRouter({ }), disable: protectedProcedure - .input(controlsInputSchema) + .input( + z.object({ + duration: z.number().optional(), + }), + ) .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole"))) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.disableAsync(input.duration); const innerHandler = dnsHoleRequestHandler.handler(integration, {}); diff --git a/packages/api/src/router/widgets/downloads.ts b/packages/api/src/router/widgets/downloads.ts index c0a72d577..10eb7c021 100644 --- a/packages/api/src/router/widgets/downloads.ts +++ b/packages/api/src/router/widgets/downloads.ts @@ -6,7 +6,7 @@ import type { Integration } from "@homarr/db/schema"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; -import { downloadClientItemSchema, integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync, downloadClientItemSchema } from "@homarr/integrations"; import { downloadClientRequestHandler } from "@homarr/request-handler/downloads"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -69,7 +69,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.pauseQueueAsync(); }), ); @@ -80,7 +80,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.pauseItemAsync(input.item); }), ); @@ -90,7 +90,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.resumeQueueAsync(); }), ); @@ -101,7 +101,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.resumeItemAsync(input.item); }), ); @@ -112,7 +112,7 @@ export const downloadsRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); await integrationInstance.deleteItemAsync(input.item, input.fromDisk); }), ); diff --git a/packages/api/src/router/widgets/indexer-manager.ts b/packages/api/src/router/widgets/indexer-manager.ts index 2bbf1356d..74bffb2af 100644 --- a/packages/api/src/router/widgets/indexer-manager.ts +++ b/packages/api/src/router/widgets/indexer-manager.ts @@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server"; import { observable } from "@trpc/server/observable"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; import { logger } from "@homarr/log"; import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager"; @@ -59,7 +59,7 @@ export const indexerManagerRouter = createTRPCRouter({ .mutation(async ({ ctx }) => { await Promise.all( ctx.integrations.map(async (integration) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.testAllAsync().catch((err) => { logger.error("indexer-manager router - ", err); throw new TRPCError({ diff --git a/packages/api/src/router/widgets/media-requests.ts b/packages/api/src/router/widgets/media-requests.ts index 9f024b30a..4af50a583 100644 --- a/packages/api/src/router/widgets/media-requests.ts +++ b/packages/api/src/router/widgets/media-requests.ts @@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list"; import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats"; @@ -94,7 +94,7 @@ export const mediaRequestsRouter = createTRPCRouter({ .unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest"))) .input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) })) .mutation(async ({ ctx: { integration }, input }) => { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const innerHandler = mediaRequestListRequestHandler.handler(integration, {}); if (input.answer === "approve") { diff --git a/packages/api/src/router/widgets/smart-home.ts b/packages/api/src/router/widgets/smart-home.ts index 13979ad28..2294bf6bb 100644 --- a/packages/api/src/router/widgets/smart-home.ts +++ b/packages/api/src/router/widgets/smart-home.ts @@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable"; import { z } from "zod"; import { getIntegrationKindsByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state"; import type { IntegrationAction } from "../../middlewares/integration"; @@ -45,7 +45,7 @@ export const smartHomeRouter = createTRPCRouter({ .unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .input(z.object({ entityId: z.string() })) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); const success = await client.triggerToggleAsync(input.entityId); const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId }); @@ -57,7 +57,7 @@ export const smartHomeRouter = createTRPCRouter({ .unstable_concat(createSmartHomeIntegrationMiddleware("interact")) .input(z.object({ automationId: z.string() })) .mutation(async ({ ctx: { integration }, input }) => { - const client = integrationCreator(integration); + const client = await createIntegrationAsync(integration); await client.triggerAutomationAsync(input.automationId); }), }); diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index af2a8d2bd..21df2a7e9 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -21,13 +21,13 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration" import { TdarrIntegration } from "../media-transcoding/tdarr-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; import { OverseerrIntegration } from "../overseerr/overseerr-integration"; -import { PiHoleIntegration } from "../pi-hole/pi-hole-integration"; +import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory"; import { PlexIntegration } from "../plex/plex-integration"; import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration"; import { ProxmoxIntegration } from "../proxmox/proxmox-integration"; import type { Integration, IntegrationInput } from "./integration"; -export const integrationCreator = ( +export const createIntegrationAsync = async ( integration: IntegrationInput & { kind: TKind }, ) => { if (!(integration.kind in integrationCreators)) { @@ -36,15 +36,22 @@ export const integrationCreator = ; + const creator = integrationCreators[integration.kind]; + + // factories are an array, to differentiate in js between class constructors and functions + if (Array.isArray(creator)) { + return (await creator[0](integration)) as IntegrationInstanceOfKind; + } + + return new creator(integration) as IntegrationInstanceOfKind; }; -export const integrationCreatorFromSecrets = ( +export const createIntegrationAsyncFromSecrets = ( integration: Modify & { secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[]; }, ) => { - return integrationCreator({ + return createIntegrationAsync({ ...integration, decryptedSecrets: integration.secrets.map((secret) => ({ ...secret, @@ -53,8 +60,11 @@ export const integrationCreatorFromSecrets = Integration; + +// factories are an array, to differentiate in js between class constructors and functions export const integrationCreators = { - piHole: PiHoleIntegration, + piHole: [createPiHoleIntegrationAsync], adGuardHome: AdGuardHomeIntegration, homeAssistant: HomeAssistantIntegration, jellyfin: JellyfinIntegration, @@ -76,4 +86,12 @@ export const integrationCreators = { tdarr: TdarrIntegration, proxmox: ProxmoxIntegration, emby: EmbyIntegration, -} satisfies Record Integration>; +} satisfies Record Promise]>; + +type IntegrationInstanceOfKind = { + [kind in TKind]: (typeof integrationCreators)[kind] extends [(input: IntegrationInput) => Promise] + ? Awaited> + : (typeof integrationCreators)[kind] extends IntegrationInstance + ? InstanceType<(typeof integrationCreators)[kind]> + : never; +}[TKind]; diff --git a/packages/integrations/src/base/error.ts b/packages/integrations/src/base/error.ts new file mode 100644 index 000000000..46fe913cf --- /dev/null +++ b/packages/integrations/src/base/error.ts @@ -0,0 +1,47 @@ +import type { Response as UndiciResponse } from "undici"; +import type { z } from "zod"; + +import type { IntegrationInput } from "./integration"; + +export class ParseError extends Error { + public readonly zodError: z.ZodError; + public readonly input: unknown; + + constructor(dataName: string, zodError: z.ZodError, input?: unknown) { + super(`Failed to parse ${dataName}`); + this.zodError = zodError; + this.input = input; + } +} + +export class ResponseError extends Error { + public readonly statusCode: number; + public readonly url: string; + public readonly content?: string; + + constructor(response: Response | UndiciResponse, content: unknown) { + super("Response failed"); + + this.statusCode = response.status; + this.url = response.url; + + try { + this.content = JSON.stringify(content); + } catch { + this.content = content as string; + } + } +} + +export class IntegrationResponseError extends ResponseError { + public readonly integration: Pick; + + constructor(integration: IntegrationInput, response: Response | UndiciResponse, content: unknown) { + super(response, content); + this.integration = { + id: integration.id, + name: integration.name, + url: integration.url, + }; + } +} diff --git a/packages/integrations/src/base/session-store.ts b/packages/integrations/src/base/session-store.ts new file mode 100644 index 000000000..da80d55f0 --- /dev/null +++ b/packages/integrations/src/base/session-store.ts @@ -0,0 +1,29 @@ +import { decryptSecret, encryptSecret } from "@homarr/common/server"; +import { logger } from "@homarr/log"; +import { createGetSetChannel } from "@homarr/redis"; + +const localLogger = logger.child({ module: "SessionStore" }); + +export const createSessionStore = (integration: { id: string }) => { + const channelName = `session-store:${integration.id}`; + const channel = createGetSetChannel<`${string}.${string}`>(channelName); + + return { + async getAsync() { + localLogger.debug("Getting session from store", { store: channelName }); + const value = await channel.getAsync(); + if (!value) return null; + return decryptSecret(value); + }, + async setAsync(value: string) { + localLogger.debug("Updating session in store", { store: channelName }); + await channel.setAsync(encryptSecret(value)); + }, + async clearAsync() { + localLogger.debug("Cleared session in store", { store: channelName }); + await channel.removeAsync(); + }, + }; +}; + +export type SessionStore = ReturnType; diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index baa5a99b0..8a52261c3 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -13,7 +13,8 @@ export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration"; export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration"; export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration"; export { OverseerrIntegration } from "./overseerr/overseerr-integration"; -export { PiHoleIntegration } from "./pi-hole/pi-hole-integration"; +export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5"; +export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6"; export { PlexIntegration } from "./plex/plex-integration"; export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration"; export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration"; @@ -36,5 +37,5 @@ export type { TdarrWorker } from "./interfaces/media-transcoding/workers"; export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items"; // Helpers -export { integrationCreator, integrationCreatorFromSecrets } from "./base/creator"; +export { createIntegrationAsync, createIntegrationAsyncFromSecrets } from "./base/creator"; export { IntegrationTestConnectionError } from "./base/test-connection-error"; diff --git a/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts b/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts index d25be7fd0..0d0d189ac 100644 --- a/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts +++ b/packages/integrations/src/interfaces/dns-hole-summary/dns-hole-summary-integration.ts @@ -2,4 +2,6 @@ import type { DnsHoleSummary } from "./dns-hole-summary-types"; export interface DnsHoleSummaryIntegration { getSummaryAsync(): Promise; + enableAsync(): Promise; + disableAsync(duration?: number): Promise; } diff --git a/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts new file mode 100644 index 000000000..7970d7069 --- /dev/null +++ b/packages/integrations/src/pi-hole/pi-hole-integration-factory.ts @@ -0,0 +1,22 @@ +import { removeTrailingSlash } from "@homarr/common"; + +import type { IntegrationInput } from "../base/integration"; +import { PiHoleIntegrationV5 } from "./v5/pi-hole-integration-v5"; +import { PiHoleIntegrationV6 } from "./v6/pi-hole-integration-v6"; + +export const createPiHoleIntegrationAsync = async (input: IntegrationInput) => { + const baseUrl = removeTrailingSlash(input.url); + const url = new URL(`${baseUrl}/api/info/version`); + const response = await fetch(url); + + /** + * In pi-hole 5 the api was at /admin/api.php, in pi-hole 6 it was moved to /api + * For the /api/info/version endpoint, the response is 404 in pi-hole 5 + * and 401 in pi-hole 6 + */ + if (response.status === 404) { + return new PiHoleIntegrationV5(input); + } + + return new PiHoleIntegrationV6(input); +}; diff --git a/packages/integrations/src/pi-hole/pi-hole-integration.ts b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts similarity index 84% rename from packages/integrations/src/pi-hole/pi-hole-integration.ts rename to packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts index 45090a977..3f3e263d8 100644 --- a/packages/integrations/src/pi-hole/pi-hole-integration.ts +++ b/packages/integrations/src/pi-hole/v5/pi-hole-integration-v5.ts @@ -1,12 +1,12 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; -import { Integration } from "../base/integration"; -import { IntegrationTestConnectionError } from "../base/test-connection-error"; -import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration"; -import type { DnsHoleSummary } from "../interfaces/dns-hole-summary/dns-hole-summary-types"; -import { summaryResponseSchema } from "./pi-hole-types"; +import { Integration } from "../../base/integration"; +import { IntegrationTestConnectionError } from "../../base/test-connection-error"; +import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { DnsHoleSummary } from "../../interfaces/dns-hole-summary/dns-hole-summary-types"; +import { summaryResponseSchema } from "./pi-hole-schemas-v5"; -export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration { +export class PiHoleIntegrationV5 extends Integration implements DnsHoleSummaryIntegration { public async getSummaryAsync(): Promise { const apiKey = super.getSecretValue("apiKey"); const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?summaryRaw", { auth: apiKey })); diff --git a/packages/integrations/src/pi-hole/pi-hole-types.ts b/packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts similarity index 75% rename from packages/integrations/src/pi-hole/pi-hole-types.ts rename to packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts index 7c9785d1b..35c204f9e 100644 --- a/packages/integrations/src/pi-hole/pi-hole-types.ts +++ b/packages/integrations/src/pi-hole/v5/pi-hole-schemas-v5.ts @@ -7,7 +7,3 @@ export const summaryResponseSchema = z.object({ dns_queries_today: z.number(), ads_percentage_today: z.number(), }); - -export const controlsInputSchema = z.object({ - duration: z.number().optional(), -}); diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts new file mode 100644 index 000000000..02b1aa8e0 --- /dev/null +++ b/packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts @@ -0,0 +1,204 @@ +import type { Response as UndiciResponse } from "undici"; +import type { z } from "zod"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; +import { extractErrorMessage } from "@homarr/common"; +import { logger } from "@homarr/log"; + +import { IntegrationResponseError, ParseError, ResponseError } from "../../base/error"; +import type { IntegrationInput } from "../../base/integration"; +import { Integration } from "../../base/integration"; +import type { SessionStore } from "../../base/session-store"; +import { createSessionStore } from "../../base/session-store"; +import { IntegrationTestConnectionError } from "../../base/test-connection-error"; +import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { DnsHoleSummary } from "../../types"; +import { dnsBlockingGetSchema, sessionResponseSchema, statsSummaryGetSchema } from "./pi-hole-schemas-v6"; + +const localLogger = logger.child({ module: "PiHoleIntegrationV6" }); + +export class PiHoleIntegrationV6 extends Integration implements DnsHoleSummaryIntegration { + private readonly sessionStore: SessionStore; + + constructor(integration: IntegrationInput) { + super(integration); + this.sessionStore = createSessionStore(integration); + } + + public async getDnsBlockingStatusAsync(): Promise> { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + + const result = dnsBlockingGetSchema.safeParse(await response.json()); + + if (!result.success) { + throw new ParseError("DNS blocking status", result.error, await response.json()); + } + + return result.data; + } + + private async getStatsSummaryAsync(): Promise> { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/stats/summary"), { + headers: { + sid: sessionId, + }, + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + + const data = await response.json(); + const result = statsSummaryGetSchema.safeParse(data); + + if (!result.success) { + throw new ParseError("stats summary", result.error, data); + } + + return result.data; + } + + public async getSummaryAsync(): Promise { + const dnsStatsSummary = await this.getStatsSummaryAsync(); + const dnsBlockingStatus = await this.getDnsBlockingStatusAsync(); + + return { + status: dnsBlockingStatus.blocking, + adsBlockedToday: dnsStatsSummary.queries.blocked, + adsBlockedTodayPercentage: dnsStatsSummary.queries.percent_blocked, + domainsBeingBlocked: dnsStatsSummary.gravity.domains_being_blocked, + dnsQueriesToday: dnsStatsSummary.queries.total, + }; + } + + public async testConnectionAsync(): Promise { + try { + const sessionId = await this.getSessionAsync(); + await this.clearSessionAsync(sessionId); + } catch (error: unknown) { + if (error instanceof ParseError) { + throw new IntegrationTestConnectionError("invalidJson"); + } + + if (error instanceof ResponseError && error.statusCode === 401) { + throw new IntegrationTestConnectionError("invalidCredentials"); + } + + throw new IntegrationTestConnectionError("commonError", extractErrorMessage(error)); + } + } + + public async enableAsync(): Promise { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + body: JSON.stringify({ blocking: true }), + method: "POST", + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + } + + public async disableAsync(duration?: number): Promise { + const response = await this.withAuthAsync(async (sessionId) => { + return await fetchWithTrustedCertificatesAsync(this.url("/api/dns/blocking"), { + headers: { + sid: sessionId, + }, + body: JSON.stringify({ blocking: false, timer: duration }), + method: "POST", + }); + }); + + if (!response.ok) { + throw new IntegrationResponseError(this.integration, response, await response.json()); + } + } + + /** + * Run the callback with the current session id + * @param callback + * @returns + */ + private async withAuthAsync(callback: (sessionId: string) => Promise) { + const storedSession = await this.sessionStore.getAsync(); + + if (storedSession) { + localLogger.debug("Using stored session for request", { integrationId: this.integration.id }); + const response = await callback(storedSession); + if (response.status !== 401) { + return response; + } + + localLogger.info("Session expired, getting new session", { integrationId: this.integration.id }); + } + + const sessionId = await this.getSessionAsync(); + await this.sessionStore.setAsync(sessionId); + const response = await callback(sessionId); + return response; + } + + /** + * Get a session id from the Pi-hole server + * @returns The session id + */ + private async getSessionAsync(): Promise { + const apiKey = super.getSecretValue("apiKey"); + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/auth"), { + method: "POST", + body: JSON.stringify({ password: apiKey }), + headers: { + "User-Agent": "Homarr", + }, + }); + const data = await response.json(); + const result = sessionResponseSchema.safeParse(data); + if (!result.success) { + throw new ParseError("session response", result.error, data); + } + if (!result.data.session.sid) { + throw new IntegrationResponseError(this.integration, response, data); + } + + localLogger.info("Received session id successfully", { integrationId: this.integration.id }); + + return result.data.session.sid; + } + + /** + * Remove the session from the Pi-hole server + * @param sessionId The session id to remove + */ + private async clearSessionAsync(sessionId: string) { + const response = await fetchWithTrustedCertificatesAsync(this.url("/api/auth"), { + method: "DELETE", + headers: { + sid: sessionId, + }, + }); + + if (!response.ok) { + localLogger.warn("Failed to clear session", { statusCode: response.status, content: await response.text() }); + } + + logger.debug("Cleared session successfully"); + } +} diff --git a/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts b/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts new file mode 100644 index 000000000..283a8a71f --- /dev/null +++ b/packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const sessionResponseSchema = z.object({ + session: z.object({ + sid: z.string().nullable(), + message: z.string().nullable(), + }), +}); + +export const dnsBlockingGetSchema = z.object({ + blocking: z.enum(["enabled", "disabled", "failed", "unknown"]).transform((value) => { + if (value === "failed") return undefined; + if (value === "unknown") return undefined; + return value; + }), + timer: z.number().nullable(), +}); + +export const statsSummaryGetSchema = z.object({ + queries: z.object({ + total: z.number(), + blocked: z.number(), + percent_blocked: z.number(), + }), + gravity: z.object({ + domains_being_blocked: z.number(), + }), +}); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index d3760cadb..66a3d0de2 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -3,7 +3,6 @@ export * from "./interfaces/dns-hole-summary/dns-hole-summary-types"; export * from "./interfaces/health-monitoring/healt-monitoring"; export * from "./interfaces/indexer-manager/indexer"; export * from "./interfaces/media-requests/media-request"; -export * from "./pi-hole/pi-hole-types"; export * from "./base/searchable-integration"; export * from "./homeassistant/homeassistant-types"; export * from "./proxmox/proxmox-types"; diff --git a/packages/integrations/test/pi-hole.spec.ts b/packages/integrations/test/pi-hole.spec.ts index 558406ab7..f03c4b548 100644 --- a/packages/integrations/test/pi-hole.spec.ts +++ b/packages/integrations/test/pi-hole.spec.ts @@ -1,17 +1,18 @@ import type { StartedTestContainer } from "testcontainers"; import { GenericContainer, Wait } from "testcontainers"; -import { describe, expect, test } from "vitest"; +import { describe, expect, test, vi } from "vitest"; -import { PiHoleIntegration } from "../src"; +import { PiHoleIntegrationV5, PiHoleIntegrationV6 } from "../src"; +import type { SessionStore } from "../src/base/session-store"; const DEFAULT_PASSWORD = "12341234"; const DEFAULT_API_KEY = "3b1434980677dcf53fa8c4a611db3b1f0f88478790097515c0abb539102778b9"; // Some hash generated from password -describe("Pi-hole integration", () => { +describe("Pi-hole v5 integration", () => { test("getSummaryAsync should return summary from pi-hole", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY); // Act const result = await piHoleIntegration.getSummaryAsync(); @@ -28,8 +29,8 @@ describe("Pi-hole integration", () => { test("testConnectionAsync should not throw", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY); // Act const actAsync = async () => await piHoleIntegration.testConnectionAsync(); @@ -43,8 +44,8 @@ describe("Pi-hole integration", () => { test("testConnectionAsync should throw with wrong credentials", async () => { // Arrange - const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start(); - const piHoleIntegration = createPiHoleIntegration(piholeContainer, "wrong-api-key"); + const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, "wrong-api-key"); // Act const actAsync = async () => await piHoleIntegration.testConnectionAsync(); @@ -57,7 +58,118 @@ describe("Pi-hole integration", () => { }, 20_000); // Timeout of 20 seconds }); -const createPiHoleContainer = (password: string) => { +vi.mock("../src/base/session-store", () => ({ + createSessionStore: () => + ({ + async getAsync() { + return await Promise.resolve(null); + }, + async setAsync() { + return await Promise.resolve(); + }, + async clearAsync() { + return await Promise.resolve(); + }, + }) satisfies SessionStore, +})); + +describe("Pi-hole v6 integration", () => { + test("getSummaryAsync should return summary from pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + const result = await piHoleIntegration.getSummaryAsync(); + + // Assert + expect(result.status).toBe("enabled"); + expect(result.adsBlockedToday).toBe(0); + expect(result.adsBlockedTodayPercentage).toBe(0); + expect(result.dnsQueriesToday).toBe(0); + expect(result.domainsBeingBlocked).toBeGreaterThanOrEqual(0); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds + + test("enableAsync should enable pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Disable pi-hole + await piholeContainer.exec(["pihole", "disable"]); + + // Act + await piHoleIntegration.enableAsync(); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toContain("enabled"); + }, 20_000); // Timeout of 20 seconds + + test("disableAsync should disable pi-hole", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + await piHoleIntegration.disableAsync(); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toBe("disabled"); + expect(status.timer).toBe(null); + }, 20_000); // Timeout of 20 seconds + + test("disableAsync should disable pi-hole with timer", async () => { + // Arrange + const timer = 10 * 60; // 10 minutes + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + await piHoleIntegration.disableAsync(timer); + + // Assert + const status = await piHoleIntegration.getDnsBlockingStatusAsync(); + expect(status.blocking).toBe("disabled"); + expect(status.timer).toBeGreaterThan(timer - 10); + }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should not throw", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, DEFAULT_PASSWORD); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).resolves.not.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds + + test("testConnectionAsync should throw with wrong credentials", async () => { + // Arrange + const piholeContainer = await createPiHoleV6Container(DEFAULT_PASSWORD).start(); + const piHoleIntegration = createPiHoleIntegrationV6(piholeContainer, "wrong-api-key"); + + // Act + const actAsync = async () => await piHoleIntegration.testConnectionAsync(); + + // Assert + await expect(actAsync()).rejects.toThrow(); + + // Cleanup + await piholeContainer.stop(); + }, 20_000); // Timeout of 20 seconds +}); + +const createPiHoleV5Container = (password: string) => { return new GenericContainer("pihole/pihole:2024.07.0") // v5 .withEnvironment({ WEBPASSWORD: password, @@ -66,8 +178,31 @@ const createPiHoleContainer = (password: string) => { .withWaitStrategy(Wait.forLogMessage("Pi-hole Enabled")); }; -const createPiHoleIntegration = (container: StartedTestContainer, apiKey: string) => { - return new PiHoleIntegration({ +const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: string) => { + return new PiHoleIntegrationV5({ + id: "1", + decryptedSecrets: [ + { + kind: "apiKey", + value: apiKey, + }, + ], + name: "Pi hole", + url: `http://${container.getHost()}:${container.getMappedPort(80)}`, + }); +}; + +const createPiHoleV6Container = (password: string) => { + return new GenericContainer("pihole/pihole:latest") + .withEnvironment({ + FTLCONF_webserver_api_password: password, + }) + .withExposedPorts(80) + .withWaitStrategy(Wait.forHttp("/admin", 80)); +}; + +const createPiHoleIntegrationV6 = (container: StartedTestContainer, apiKey: string) => { + return new PiHoleIntegrationV6({ id: "1", decryptedSecrets: [ { diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index b752177ac..c61393e51 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -9,6 +9,7 @@ export { createChannelWithLatestAndEvents, handshakeAsync, createSubPubChannel, + createGetSetChannel, } from "./lib/channel"; export const exampleChannel = createSubPubChannel<{ message: string }>("example"); diff --git a/packages/redis/src/lib/channel.ts b/packages/redis/src/lib/channel.ts index 42eb2ef13..686fd4f50 100644 --- a/packages/redis/src/lib/channel.ts +++ b/packages/redis/src/lib/channel.ts @@ -94,6 +94,36 @@ export const createListChannel = (name: string) => { }; }; +/** + * Creates a new redis channel for getting and setting data + * @param name name of channel + */ +export const createGetSetChannel = (name: string) => { + return { + /** + * Get data from the channel + * @returns data or null if not found + */ + getAsync: async () => { + const data = await getSetClient.get(name); + return data ? superjson.parse(data) : null; + }, + /** + * Set data in the channel + * @param data data to be stored in the channel + */ + setAsync: async (data: TData) => { + await getSetClient.set(name, superjson.stringify(data)); + }, + /** + * Remove data from the channel + */ + removeAsync: async () => { + await getSetClient.del(name); + }, + }; +}; + /** * Creates a new cache channel. * @param name name of the channel diff --git a/packages/request-handler/src/calendar.ts b/packages/request-handler/src/calendar.ts index 3987e0497..f047d74df 100644 --- a/packages/request-handler/src/calendar.ts +++ b/packages/request-handler/src/calendar.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler { year: number; month: number; releaseType: RadarrReleaseType[] } >({ async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const startDate = dayjs().year(input.year).month(input.month).startOf("month"); const endDate = startDate.clone().endOf("month"); return await integrationInstance.getCalendarEventsAsync(startDate.toDate(), endDate.toDate()); diff --git a/packages/request-handler/src/dns-hole.ts b/packages/request-handler/src/dns-hole.ts index c05ee575d..a8c920801 100644 --- a/packages/request-handler/src/dns-hole.ts +++ b/packages/request-handler/src/dns-hole.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { DnsHoleSummary } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSummaryAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/downloads.ts b/packages/request-handler/src/downloads.ts index 71fc0d4a4..567b88324 100644 --- a/packages/request-handler/src/downloads.ts +++ b/packages/request-handler/src/downloads.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { DownloadClientJobsAndStatus } from "@homarr/integrations"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const downloadClientRequestHandler = createCachedIntegrationRequestHandle Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClientJobsAndStatusAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index f57b9ee30..48d95cd20 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getSystemInfoAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), @@ -25,7 +25,7 @@ export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getClusterInfoAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/indexer-manager.ts b/packages/request-handler/src/indexer-manager.ts index 5262fa8f0..a957b32ad 100644 --- a/packages/request-handler/src/indexer-manager.ts +++ b/packages/request-handler/src/indexer-manager.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { Indexer } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const indexerManagerRequestHandler = createCachedIntegrationRequestHandle Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getIndexersAsync(); }, cacheDuration: dayjs.duration(5, "minutes"), diff --git a/packages/request-handler/src/media-request-list.ts b/packages/request-handler/src/media-request-list.ts index 53c31a7b8..fd4e02234 100644 --- a/packages/request-handler/src/media-request-list.ts +++ b/packages/request-handler/src/media-request-list.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequest } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaRequestListRequestHandler = createCachedIntegrationRequestHand Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getRequestsAsync(); }, cacheDuration: dayjs.duration(1, "minute"), diff --git a/packages/request-handler/src/media-request-stats.ts b/packages/request-handler/src/media-request-stats.ts index c4829167a..a6568aecd 100644 --- a/packages/request-handler/src/media-request-stats.ts +++ b/packages/request-handler/src/media-request-stats.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { MediaRequestStats } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHan Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return { stats: await integrationInstance.getStatsAsync(), users: await integrationInstance.getUsersAsync(), diff --git a/packages/request-handler/src/media-server.ts b/packages/request-handler/src/media-server.ts index e2f52773f..2fadad23f 100644 --- a/packages/request-handler/src/media-server.ts +++ b/packages/request-handler/src/media-server.ts @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import type { StreamSession } from "@homarr/integrations"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -12,7 +12,7 @@ export const mediaServerRequestHandler = createCachedIntegrationRequestHandler< Record >({ async requestAsync(integration, _input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return await integrationInstance.getCurrentSessionsAsync(); }, cacheDuration: dayjs.duration(5, "seconds"), diff --git a/packages/request-handler/src/media-transcoding.ts b/packages/request-handler/src/media-transcoding.ts index 05446bdd6..c40d5b058 100644 --- a/packages/request-handler/src/media-transcoding.ts +++ b/packages/request-handler/src/media-transcoding.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -14,7 +14,7 @@ export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHand queryKey: "mediaTranscoding", cacheDuration: dayjs.duration(5, "minutes"), async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); return { queue: await integrationInstance.getQueueAsync(input.pageOffset, input.pageSize), workers: await integrationInstance.getWorkersAsync(), diff --git a/packages/request-handler/src/smart-home-entity-state.ts b/packages/request-handler/src/smart-home-entity-state.ts index 69641949c..5fe02b2ac 100644 --- a/packages/request-handler/src/smart-home-entity-state.ts +++ b/packages/request-handler/src/smart-home-entity-state.ts @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; -import { integrationCreator } from "@homarr/integrations"; +import { createIntegrationAsync } from "@homarr/integrations"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; @@ -11,7 +11,7 @@ export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequest { entityId: string } >({ async requestAsync(integration, input) { - const integrationInstance = integrationCreator(integration); + const integrationInstance = await createIntegrationAsync(integration); const result = await integrationInstance.getEntityStateAsync(input.entityId); if (!result.success) {