feat(pihole): add support for v6 (#2448)
* feat(pihole): add support for v6 * fix: add session-store to keep using same session for pi-hole requests * chore: address pull request feedback * fix: import issue * fix: other import errors
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
integrationKinds,
|
integrationKinds,
|
||||||
integrationSecretKindObject,
|
integrationSecretKindObject,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
@@ -465,7 +465,7 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
|
.unstable_concat(createOneIntegrationMiddleware("query", ...getIntegrationKindsByCategory("search")))
|
||||||
.input(z.object({ integrationId: z.string(), query: z.string() }))
|
.input(z.object({ integrationId: z.string(), query: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const integrationInstance = integrationCreator(ctx.integration);
|
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
||||||
return await integrationInstance.searchAsync(encodeURI(input.query));
|
return await integrationInstance.searchAsync(encodeURI(input.query));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { decryptSecret } from "@homarr/common/server";
|
|||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import { getAllSecretKindOptions } 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";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
type FormIntegration = Integration & {
|
type FormIntegration = Integration & {
|
||||||
@@ -66,7 +66,7 @@ export const testConnectionAsync = async (
|
|||||||
|
|
||||||
const { secrets: _, ...baseIntegration } = integration;
|
const { secrets: _, ...baseIntegration } = integration;
|
||||||
|
|
||||||
const integrationInstance = integrationCreator({
|
const integrationInstance = await createIntegrationAsync({
|
||||||
...baseIntegration,
|
...baseIntegration,
|
||||||
decryptedSecrets,
|
decryptedSecrets,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|||||||
import { asc, createId, eq, like } from "@homarr/db";
|
import { asc, createId, eq, like } from "@homarr/db";
|
||||||
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
import { getServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||||
import { searchEngines, users } from "@homarr/db/schema";
|
import { searchEngines, users } from "@homarr/db/schema";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
@@ -134,14 +134,14 @@ export const searchEngineRouter = createTRPCRouter({
|
|||||||
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
.unstable_concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||||
.input(validation.common.mediaRequestOptions)
|
.input(validation.common.mediaRequestOptions)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const integration = integrationCreator(ctx.integration);
|
const integration = await createIntegrationAsync(ctx.integration);
|
||||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||||
}),
|
}),
|
||||||
requestMedia: protectedProcedure
|
requestMedia: protectedProcedure
|
||||||
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
.unstable_concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||||
.input(validation.common.requestMedia)
|
.input(validation.common.requestMedia)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.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);
|
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||||
}),
|
}),
|
||||||
create: permissionRequiredProcedure
|
create: permissionRequiredProcedure
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ vi.mock("@homarr/common/server", async (importActual) => {
|
|||||||
describe("testConnectionAsync should run test connection of integration", () => {
|
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 () => {
|
test("with input of only form secrets matching api key kind it should use form apiKey", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
factorySpy.mockReturnValue({
|
factorySpy.mockReturnValue(
|
||||||
testConnectionAsync: async () => await Promise.resolve(),
|
Promise.resolve({
|
||||||
} as homarrIntegrations.PiHoleIntegration);
|
testConnectionAsync: async () => await Promise.resolve(),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||||
|
|
||||||
const integration = {
|
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 () => {
|
test("with input of only null form secrets and the required db secrets matching api key kind it should use db apiKey", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
factorySpy.mockReturnValue({
|
factorySpy.mockReturnValue(
|
||||||
testConnectionAsync: async () => await Promise.resolve(),
|
Promise.resolve({
|
||||||
} as homarrIntegrations.PiHoleIntegration);
|
testConnectionAsync: async () => await Promise.resolve(),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||||
|
|
||||||
const integration = {
|
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 () => {
|
test("with input of form and db secrets matching api key kind it should use form apiKey", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
factorySpy.mockReturnValue({
|
factorySpy.mockReturnValue(
|
||||||
testConnectionAsync: async () => await Promise.resolve(),
|
Promise.resolve({
|
||||||
} as homarrIntegrations.PiHoleIntegration);
|
testConnectionAsync: async () => await Promise.resolve(),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
optionsSpy.mockReturnValue([["apiKey"]]);
|
optionsSpy.mockReturnValue([["apiKey"]]);
|
||||||
|
|
||||||
const integration = {
|
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 () => {
|
test("with input of form apiKey and db secrets for username and password it should use form apiKey when both is allowed", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
factorySpy.mockReturnValue({
|
factorySpy.mockReturnValue(
|
||||||
testConnectionAsync: async () => await Promise.resolve(),
|
Promise.resolve({
|
||||||
} as homarrIntegrations.PiHoleIntegration);
|
testConnectionAsync: async () => await Promise.resolve(),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
||||||
|
|
||||||
const integration = {
|
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 () => {
|
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
|
// Arrange
|
||||||
const factorySpy = vi.spyOn(homarrIntegrations, "integrationCreator");
|
const factorySpy = vi.spyOn(homarrIntegrations, "createIntegrationAsync");
|
||||||
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
const optionsSpy = vi.spyOn(homarrDefinitions, "getAllSecretKindOptions");
|
||||||
factorySpy.mockReturnValue({
|
factorySpy.mockReturnValue(
|
||||||
testConnectionAsync: async () => await Promise.resolve(),
|
Promise.resolve({
|
||||||
} as homarrIntegrations.PiHoleIntegration);
|
testConnectionAsync: async () => await Promise.resolve(),
|
||||||
|
} as homarrIntegrations.PiHoleIntegrationV6),
|
||||||
|
);
|
||||||
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
optionsSpy.mockReturnValue([["username", "password"], ["apiKey"]]);
|
||||||
|
|
||||||
const integration = {
|
const integration = {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import type { Modify } from "@homarr/common/types";
|
import type { Modify } from "@homarr/common/types";
|
||||||
import type { Integration } from "@homarr/db/schema";
|
import type { Integration } from "@homarr/db/schema";
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { getIntegrationKindsByCategory } 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 type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||||
import { controlsInputSchema } from "@homarr/integrations/types";
|
|
||||||
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
|
import { dnsHoleRequestHandler } from "@homarr/request-handler/dns-hole";
|
||||||
|
|
||||||
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
import { createManyIntegrationMiddleware, createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||||
@@ -65,7 +65,7 @@ export const dnsHoleRouter = createTRPCRouter({
|
|||||||
enable: protectedProcedure
|
enable: protectedProcedure
|
||||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||||
.mutation(async ({ ctx: { integration } }) => {
|
.mutation(async ({ ctx: { integration } }) => {
|
||||||
const client = integrationCreator(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
await client.enableAsync();
|
await client.enableAsync();
|
||||||
|
|
||||||
const innerHandler = dnsHoleRequestHandler.handler(integration, {});
|
const innerHandler = dnsHoleRequestHandler.handler(integration, {});
|
||||||
@@ -76,10 +76,14 @@ export const dnsHoleRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
disable: protectedProcedure
|
disable: protectedProcedure
|
||||||
.input(controlsInputSchema)
|
.input(
|
||||||
|
z.object({
|
||||||
|
duration: z.number().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("dnsHole")))
|
||||||
.mutation(async ({ ctx: { integration }, input }) => {
|
.mutation(async ({ ctx: { integration }, input }) => {
|
||||||
const client = integrationCreator(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
await client.disableAsync(input.duration);
|
await client.disableAsync(input.duration);
|
||||||
|
|
||||||
const innerHandler = dnsHoleRequestHandler.handler(integration, {});
|
const innerHandler = dnsHoleRequestHandler.handler(integration, {});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { Integration } from "@homarr/db/schema";
|
|||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
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 { downloadClientRequestHandler } from "@homarr/request-handler/downloads";
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
@@ -69,7 +69,7 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx }) => {
|
.mutation(async ({ ctx }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
await integrationInstance.pauseQueueAsync();
|
await integrationInstance.pauseQueueAsync();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -80,7 +80,7 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
await integrationInstance.pauseItemAsync(input.item);
|
await integrationInstance.pauseItemAsync(input.item);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -90,7 +90,7 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx }) => {
|
.mutation(async ({ ctx }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
await integrationInstance.resumeQueueAsync();
|
await integrationInstance.resumeQueueAsync();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -101,7 +101,7 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
await integrationInstance.resumeItemAsync(input.item);
|
await integrationInstance.resumeItemAsync(input.item);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -112,7 +112,7 @@ export const downloadsRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
await integrationInstance.deleteItemAsync(input.item, input.fromDisk);
|
await integrationInstance.deleteItemAsync(input.item, input.fromDisk);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { observable } from "@trpc/server/observable";
|
import { observable } from "@trpc/server/observable";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { Indexer } from "@homarr/integrations/types";
|
import type { Indexer } from "@homarr/integrations/types";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
|
import { indexerManagerRequestHandler } from "@homarr/request-handler/indexer-manager";
|
||||||
@@ -59,7 +59,7 @@ export const indexerManagerRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx }) => {
|
.mutation(async ({ ctx }) => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
const client = integrationCreator(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
await client.testAllAsync().catch((err) => {
|
await client.testAllAsync().catch((err) => {
|
||||||
logger.error("indexer-manager router - ", err);
|
logger.error("indexer-manager router - ", err);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { MediaRequest } from "@homarr/integrations/types";
|
import type { MediaRequest } from "@homarr/integrations/types";
|
||||||
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
||||||
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
||||||
@@ -94,7 +94,7 @@ export const mediaRequestsRouter = createTRPCRouter({
|
|||||||
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
|
.unstable_concat(createOneIntegrationMiddleware("interact", ...getIntegrationKindsByCategory("mediaRequest")))
|
||||||
.input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) }))
|
.input(z.object({ requestId: z.number(), answer: z.enum(["approve", "decline"]) }))
|
||||||
.mutation(async ({ ctx: { integration }, input }) => {
|
.mutation(async ({ ctx: { integration }, input }) => {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
const innerHandler = mediaRequestListRequestHandler.handler(integration, {});
|
const innerHandler = mediaRequestListRequestHandler.handler(integration, {});
|
||||||
|
|
||||||
if (input.answer === "approve") {
|
if (input.answer === "approve") {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { observable } from "@trpc/server/observable";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
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 { smartHomeEntityStateRequestHandler } from "@homarr/request-handler/smart-home-entity-state";
|
||||||
|
|
||||||
import type { IntegrationAction } from "../../middlewares/integration";
|
import type { IntegrationAction } from "../../middlewares/integration";
|
||||||
@@ -45,7 +45,7 @@ export const smartHomeRouter = createTRPCRouter({
|
|||||||
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||||
.input(z.object({ entityId: z.string() }))
|
.input(z.object({ entityId: z.string() }))
|
||||||
.mutation(async ({ ctx: { integration }, input }) => {
|
.mutation(async ({ ctx: { integration }, input }) => {
|
||||||
const client = integrationCreator(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
const success = await client.triggerToggleAsync(input.entityId);
|
const success = await client.triggerToggleAsync(input.entityId);
|
||||||
|
|
||||||
const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId });
|
const innerHandler = smartHomeEntityStateRequestHandler.handler(integration, { entityId: input.entityId });
|
||||||
@@ -57,7 +57,7 @@ export const smartHomeRouter = createTRPCRouter({
|
|||||||
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
.unstable_concat(createSmartHomeIntegrationMiddleware("interact"))
|
||||||
.input(z.object({ automationId: z.string() }))
|
.input(z.object({ automationId: z.string() }))
|
||||||
.mutation(async ({ ctx: { integration }, input }) => {
|
.mutation(async ({ ctx: { integration }, input }) => {
|
||||||
const client = integrationCreator(integration);
|
const client = await createIntegrationAsync(integration);
|
||||||
await client.triggerAutomationAsync(input.automationId);
|
await client.triggerAutomationAsync(input.automationId);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"
|
|||||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||||
import { OverseerrIntegration } from "../overseerr/overseerr-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 { PlexIntegration } from "../plex/plex-integration";
|
||||||
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
|
||||||
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
import { ProxmoxIntegration } from "../proxmox/proxmox-integration";
|
||||||
import type { Integration, IntegrationInput } from "./integration";
|
import type { Integration, IntegrationInput } from "./integration";
|
||||||
|
|
||||||
export const integrationCreator = <TKind extends keyof typeof integrationCreators>(
|
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
|
||||||
integration: IntegrationInput & { kind: TKind },
|
integration: IntegrationInput & { kind: TKind },
|
||||||
) => {
|
) => {
|
||||||
if (!(integration.kind in integrationCreators)) {
|
if (!(integration.kind in integrationCreators)) {
|
||||||
@@ -36,15 +36,22 @@ export const integrationCreator = <TKind extends keyof typeof integrationCreator
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new integrationCreators[integration.kind](integration) as InstanceType<(typeof integrationCreators)[TKind]>;
|
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<TKind>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new creator(integration) as IntegrationInstanceOfKind<TKind>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const integrationCreatorFromSecrets = <TKind extends keyof typeof integrationCreators>(
|
export const createIntegrationAsyncFromSecrets = <TKind extends keyof typeof integrationCreators>(
|
||||||
integration: Modify<DbIntegration, { kind: TKind }> & {
|
integration: Modify<DbIntegration, { kind: TKind }> & {
|
||||||
secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[];
|
secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[];
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
return integrationCreator({
|
return createIntegrationAsync({
|
||||||
...integration,
|
...integration,
|
||||||
decryptedSecrets: integration.secrets.map((secret) => ({
|
decryptedSecrets: integration.secrets.map((secret) => ({
|
||||||
...secret,
|
...secret,
|
||||||
@@ -53,8 +60,11 @@ export const integrationCreatorFromSecrets = <TKind extends keyof typeof integra
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type IntegrationInstance = new (integration: IntegrationInput) => Integration;
|
||||||
|
|
||||||
|
// factories are an array, to differentiate in js between class constructors and functions
|
||||||
export const integrationCreators = {
|
export const integrationCreators = {
|
||||||
piHole: PiHoleIntegration,
|
piHole: [createPiHoleIntegrationAsync],
|
||||||
adGuardHome: AdGuardHomeIntegration,
|
adGuardHome: AdGuardHomeIntegration,
|
||||||
homeAssistant: HomeAssistantIntegration,
|
homeAssistant: HomeAssistantIntegration,
|
||||||
jellyfin: JellyfinIntegration,
|
jellyfin: JellyfinIntegration,
|
||||||
@@ -76,4 +86,12 @@ export const integrationCreators = {
|
|||||||
tdarr: TdarrIntegration,
|
tdarr: TdarrIntegration,
|
||||||
proxmox: ProxmoxIntegration,
|
proxmox: ProxmoxIntegration,
|
||||||
emby: EmbyIntegration,
|
emby: EmbyIntegration,
|
||||||
} satisfies Record<IntegrationKind, new (integration: IntegrationInput) => Integration>;
|
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||||
|
|
||||||
|
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||||
|
[kind in TKind]: (typeof integrationCreators)[kind] extends [(input: IntegrationInput) => Promise<Integration>]
|
||||||
|
? Awaited<ReturnType<(typeof integrationCreators)[kind][0]>>
|
||||||
|
: (typeof integrationCreators)[kind] extends IntegrationInstance
|
||||||
|
? InstanceType<(typeof integrationCreators)[kind]>
|
||||||
|
: never;
|
||||||
|
}[TKind];
|
||||||
|
|||||||
47
packages/integrations/src/base/error.ts
Normal file
47
packages/integrations/src/base/error.ts
Normal file
@@ -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<IntegrationInput, "id" | "name" | "url">;
|
||||||
|
|
||||||
|
constructor(integration: IntegrationInput, response: Response | UndiciResponse, content: unknown) {
|
||||||
|
super(response, content);
|
||||||
|
this.integration = {
|
||||||
|
id: integration.id,
|
||||||
|
name: integration.name,
|
||||||
|
url: integration.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
29
packages/integrations/src/base/session-store.ts
Normal file
29
packages/integrations/src/base/session-store.ts
Normal file
@@ -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<typeof createSessionStore>;
|
||||||
@@ -13,7 +13,8 @@ export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
|
|||||||
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
|
||||||
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
|
||||||
export { OverseerrIntegration } from "./overseerr/overseerr-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 { PlexIntegration } from "./plex/plex-integration";
|
||||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||||
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-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";
|
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
export { integrationCreator, integrationCreatorFromSecrets } from "./base/creator";
|
export { createIntegrationAsync, createIntegrationAsyncFromSecrets } from "./base/creator";
|
||||||
export { IntegrationTestConnectionError } from "./base/test-connection-error";
|
export { IntegrationTestConnectionError } from "./base/test-connection-error";
|
||||||
|
|||||||
@@ -2,4 +2,6 @@ import type { DnsHoleSummary } from "./dns-hole-summary-types";
|
|||||||
|
|
||||||
export interface DnsHoleSummaryIntegration {
|
export interface DnsHoleSummaryIntegration {
|
||||||
getSummaryAsync(): Promise<DnsHoleSummary>;
|
getSummaryAsync(): Promise<DnsHoleSummary>;
|
||||||
|
enableAsync(): Promise<void>;
|
||||||
|
disableAsync(duration?: number): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
|
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../../base/integration";
|
||||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
import { IntegrationTestConnectionError } from "../../base/test-connection-error";
|
||||||
import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration";
|
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 type { DnsHoleSummary } from "../../interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||||
import { summaryResponseSchema } from "./pi-hole-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<DnsHoleSummary> {
|
public async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
const response = await fetchWithTrustedCertificatesAsync(this.url("/admin/api.php?summaryRaw", { auth: apiKey }));
|
||||||
@@ -7,7 +7,3 @@ export const summaryResponseSchema = z.object({
|
|||||||
dns_queries_today: z.number(),
|
dns_queries_today: z.number(),
|
||||||
ads_percentage_today: z.number(),
|
ads_percentage_today: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const controlsInputSchema = z.object({
|
|
||||||
duration: z.number().optional(),
|
|
||||||
});
|
|
||||||
204
packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts
Normal file
204
packages/integrations/src/pi-hole/v6/pi-hole-integration-v6.ts
Normal file
@@ -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<z.infer<typeof dnsBlockingGetSchema>> {
|
||||||
|
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<z.infer<typeof statsSummaryGetSchema>> {
|
||||||
|
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<DnsHoleSummary> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<UndiciResponse>) {
|
||||||
|
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<string> {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
28
packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts
Normal file
28
packages/integrations/src/pi-hole/v6/pi-hole-schemas-v6.ts
Normal file
@@ -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(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -3,7 +3,6 @@ export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
|||||||
export * from "./interfaces/health-monitoring/healt-monitoring";
|
export * from "./interfaces/health-monitoring/healt-monitoring";
|
||||||
export * from "./interfaces/indexer-manager/indexer";
|
export * from "./interfaces/indexer-manager/indexer";
|
||||||
export * from "./interfaces/media-requests/media-request";
|
export * from "./interfaces/media-requests/media-request";
|
||||||
export * from "./pi-hole/pi-hole-types";
|
|
||||||
export * from "./base/searchable-integration";
|
export * from "./base/searchable-integration";
|
||||||
export * from "./homeassistant/homeassistant-types";
|
export * from "./homeassistant/homeassistant-types";
|
||||||
export * from "./proxmox/proxmox-types";
|
export * from "./proxmox/proxmox-types";
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { StartedTestContainer } from "testcontainers";
|
import type { StartedTestContainer } from "testcontainers";
|
||||||
import { GenericContainer, Wait } 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_PASSWORD = "12341234";
|
||||||
const DEFAULT_API_KEY = "3b1434980677dcf53fa8c4a611db3b1f0f88478790097515c0abb539102778b9"; // Some hash generated from password
|
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 () => {
|
test("getSummaryAsync should return summary from pi-hole", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start();
|
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
|
||||||
const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY);
|
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const result = await piHoleIntegration.getSummaryAsync();
|
const result = await piHoleIntegration.getSummaryAsync();
|
||||||
@@ -28,8 +29,8 @@ describe("Pi-hole integration", () => {
|
|||||||
|
|
||||||
test("testConnectionAsync should not throw", async () => {
|
test("testConnectionAsync should not throw", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start();
|
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
|
||||||
const piHoleIntegration = createPiHoleIntegration(piholeContainer, DEFAULT_API_KEY);
|
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, DEFAULT_API_KEY);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const actAsync = async () => await piHoleIntegration.testConnectionAsync();
|
const actAsync = async () => await piHoleIntegration.testConnectionAsync();
|
||||||
@@ -43,8 +44,8 @@ describe("Pi-hole integration", () => {
|
|||||||
|
|
||||||
test("testConnectionAsync should throw with wrong credentials", async () => {
|
test("testConnectionAsync should throw with wrong credentials", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const piholeContainer = await createPiHoleContainer(DEFAULT_PASSWORD).start();
|
const piholeContainer = await createPiHoleV5Container(DEFAULT_PASSWORD).start();
|
||||||
const piHoleIntegration = createPiHoleIntegration(piholeContainer, "wrong-api-key");
|
const piHoleIntegration = createPiHoleIntegrationV5(piholeContainer, "wrong-api-key");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
const actAsync = async () => await piHoleIntegration.testConnectionAsync();
|
const actAsync = async () => await piHoleIntegration.testConnectionAsync();
|
||||||
@@ -57,7 +58,118 @@ describe("Pi-hole integration", () => {
|
|||||||
}, 20_000); // Timeout of 20 seconds
|
}, 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
|
return new GenericContainer("pihole/pihole:2024.07.0") // v5
|
||||||
.withEnvironment({
|
.withEnvironment({
|
||||||
WEBPASSWORD: password,
|
WEBPASSWORD: password,
|
||||||
@@ -66,8 +178,31 @@ const createPiHoleContainer = (password: string) => {
|
|||||||
.withWaitStrategy(Wait.forLogMessage("Pi-hole Enabled"));
|
.withWaitStrategy(Wait.forLogMessage("Pi-hole Enabled"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPiHoleIntegration = (container: StartedTestContainer, apiKey: string) => {
|
const createPiHoleIntegrationV5 = (container: StartedTestContainer, apiKey: string) => {
|
||||||
return new PiHoleIntegration({
|
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",
|
id: "1",
|
||||||
decryptedSecrets: [
|
decryptedSecrets: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export {
|
|||||||
createChannelWithLatestAndEvents,
|
createChannelWithLatestAndEvents,
|
||||||
handshakeAsync,
|
handshakeAsync,
|
||||||
createSubPubChannel,
|
createSubPubChannel,
|
||||||
|
createGetSetChannel,
|
||||||
} from "./lib/channel";
|
} from "./lib/channel";
|
||||||
|
|
||||||
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
|
export const exampleChannel = createSubPubChannel<{ message: string }>("example");
|
||||||
|
|||||||
@@ -94,6 +94,36 @@ export const createListChannel = <TItem>(name: string) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new redis channel for getting and setting data
|
||||||
|
* @param name name of channel
|
||||||
|
*/
|
||||||
|
export const createGetSetChannel = <TData>(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<TData>(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.
|
* Creates a new cache channel.
|
||||||
* @param name name of the channel
|
* @param name name of the channel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
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 type { CalendarEvent, RadarrReleaseType } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const calendarMonthRequestHandler = createCachedIntegrationRequestHandler
|
|||||||
{ year: number; month: number; releaseType: RadarrReleaseType[] }
|
{ year: number; month: number; releaseType: RadarrReleaseType[] }
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, input) {
|
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 startDate = dayjs().year(input.year).month(input.month).startOf("month");
|
||||||
const endDate = startDate.clone().endOf("month");
|
const endDate = startDate.clone().endOf("month");
|
||||||
return await integrationInstance.getCalendarEventsAsync(startDate.toDate(), endDate.toDate());
|
return await integrationInstance.getCalendarEventsAsync(startDate.toDate(), endDate.toDate());
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
import type { DnsHoleSummary } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const dnsHoleRequestHandler = createCachedIntegrationRequestHandler<
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getSummaryAsync();
|
return await integrationInstance.getSummaryAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
import type { DownloadClientJobsAndStatus } from "@homarr/integrations";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export const downloadClientRequestHandler = createCachedIntegrationRequestHandle
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getClientJobsAndStatusAsync();
|
return await integrationInstance.getClientJobsAndStatusAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
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 type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getSystemInfoAsync();
|
return await integrationInstance.getSystemInfoAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
@@ -25,7 +25,7 @@ export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler<
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getClusterInfoAsync();
|
return await integrationInstance.getClusterInfoAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { Indexer } from "@homarr/integrations/types";
|
import type { Indexer } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const indexerManagerRequestHandler = createCachedIntegrationRequestHandle
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getIndexersAsync();
|
return await integrationInstance.getIndexersAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "minutes"),
|
cacheDuration: dayjs.duration(5, "minutes"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { MediaRequest } from "@homarr/integrations/types";
|
import type { MediaRequest } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const mediaRequestListRequestHandler = createCachedIntegrationRequestHand
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getRequestsAsync();
|
return await integrationInstance.getRequestsAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(1, "minute"),
|
cacheDuration: dayjs.duration(1, "minute"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import type { MediaRequestStats } from "@homarr/integrations/types";
|
import type { MediaRequestStats } from "@homarr/integrations/types";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -12,7 +12,7 @@ export const mediaRequestStatsRequestHandler = createCachedIntegrationRequestHan
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return {
|
return {
|
||||||
stats: await integrationInstance.getStatsAsync(),
|
stats: await integrationInstance.getStatsAsync(),
|
||||||
users: await integrationInstance.getUsersAsync(),
|
users: await integrationInstance.getUsersAsync(),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import type { StreamSession } from "@homarr/integrations";
|
import type { StreamSession } from "@homarr/integrations";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
|
|||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return await integrationInstance.getCurrentSessionsAsync();
|
return await integrationInstance.getCurrentSessionsAsync();
|
||||||
},
|
},
|
||||||
cacheDuration: dayjs.duration(5, "seconds"),
|
cacheDuration: dayjs.duration(5, "seconds"),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
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 type { TdarrQueue, TdarrStatistics, TdarrWorker } from "@homarr/integrations";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
@@ -14,7 +14,7 @@ export const mediaTranscodingRequestHandler = createCachedIntegrationRequestHand
|
|||||||
queryKey: "mediaTranscoding",
|
queryKey: "mediaTranscoding",
|
||||||
cacheDuration: dayjs.duration(5, "minutes"),
|
cacheDuration: dayjs.duration(5, "minutes"),
|
||||||
async requestAsync(integration, input) {
|
async requestAsync(integration, input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
return {
|
return {
|
||||||
queue: await integrationInstance.getQueueAsync(input.pageOffset, input.pageSize),
|
queue: await integrationInstance.getQueueAsync(input.pageOffset, input.pageSize),
|
||||||
workers: await integrationInstance.getWorkersAsync(),
|
workers: await integrationInstance.getWorkersAsync(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { integrationCreator } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
|
||||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export const smartHomeEntityStateRequestHandler = createCachedIntegrationRequest
|
|||||||
{ entityId: string }
|
{ entityId: string }
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, input) {
|
async requestAsync(integration, input) {
|
||||||
const integrationInstance = integrationCreator(integration);
|
const integrationInstance = await createIntegrationAsync(integration);
|
||||||
const result = await integrationInstance.getEntityStateAsync(input.entityId);
|
const result = await integrationInstance.getEntityStateAsync(input.entityId);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
|||||||
Reference in New Issue
Block a user