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:
Meier Lukas
2025-03-04 21:17:35 +01:00
committed by GitHub
parent e88b29dc9d
commit 3804d530ec
33 changed files with 624 additions and 98 deletions

View File

@@ -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 = <TKind extends keyof typeof integrationCreators>(
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
integration: IntegrationInput & { kind: TKind },
) => {
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 }> & {
secrets: { kind: IntegrationSecretKind; value: `${string}.${string}` }[];
},
) => {
return integrationCreator({
return createIntegrationAsync({
...integration,
decryptedSecrets: integration.secrets.map((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 = {
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<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];

View 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,
};
}
}

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