feat: add pi hole summary integration (#521)
* feat: add pi hole summary integration * feat: add pi hole summary widget * fix: type issues with integrations and integrationIds * feat: add middleware for integrations and improve cache redis channel * feat: add error boundary for widgets * fix: broken lock file * fix: format format issues * fix: typecheck issue * fix: deepsource issues * fix: widget sandbox without error boundary * chore: address pull request feedback * chore: remove todo comment and created issue * fix: format issues * fix: deepsource issue
This commit is contained in:
1
packages/integrations/index.ts
Normal file
1
packages/integrations/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
41
packages/integrations/package.json
Normal file
41
packages/integrations/package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "@homarr/integrations",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./types": "./src/types.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"@homarr/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@homarr/prettier-config"
|
||||
}
|
||||
22
packages/integrations/src/base/integration.ts
Normal file
22
packages/integrations/src/base/integration.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
|
||||
import type { IntegrationSecret } from "./types";
|
||||
|
||||
export abstract class Integration {
|
||||
constructor(
|
||||
protected integration: {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
decryptedSecrets: IntegrationSecret[];
|
||||
},
|
||||
) {}
|
||||
|
||||
protected getSecretValue(kind: IntegrationSecretKind) {
|
||||
const secret = this.integration.decryptedSecrets.find((secret) => secret.kind === kind);
|
||||
if (!secret) {
|
||||
throw new Error(`No secret of kind ${kind} was found`);
|
||||
}
|
||||
return secret.value;
|
||||
}
|
||||
}
|
||||
6
packages/integrations/src/base/types.ts
Normal file
6
packages/integrations/src/base/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
||||
|
||||
export interface IntegrationSecret {
|
||||
kind: IntegrationSecretKind;
|
||||
value: string;
|
||||
}
|
||||
1
packages/integrations/src/index.ts
Normal file
1
packages/integrations/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { DnsHoleSummary } from "./dns-hole-summary-types";
|
||||
|
||||
export interface DnsHoleSummaryIntegration {
|
||||
getSummaryAsync(): Promise<DnsHoleSummary>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface DnsHoleSummary {
|
||||
domainsBeingBlocked: number;
|
||||
adsBlockedToday: number;
|
||||
adsBlockedTodayPercentage: number;
|
||||
dnsQueriesToday: number;
|
||||
}
|
||||
31
packages/integrations/src/pi-hole/pi-hole-integration.ts
Normal file
31
packages/integrations/src/pi-hole/pi-hole-integration.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Integration } from "../base/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 { summaryResponseSchema } from "./pi-hole-types";
|
||||
|
||||
export class PiHoleIntegration extends Integration implements DnsHoleSummaryIntegration {
|
||||
async getSummaryAsync(): Promise<DnsHoleSummary> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetch(`${this.integration.url}/admin/api.php?summaryRaw&auth=${apiKey}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch summary for ${this.integration.name} (${this.integration.id}): ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = summaryResponseSchema.safeParse(await response.json());
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(
|
||||
`Failed to parse summary for ${this.integration.name} (${this.integration.id}), most likely your api key is wrong: ${result.error.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
adsBlockedToday: result.data.ads_blocked_today,
|
||||
adsBlockedTodayPercentage: result.data.ads_percentage_today,
|
||||
domainsBeingBlocked: result.data.domains_being_blocked,
|
||||
dnsQueriesToday: result.data.dns_queries_today,
|
||||
};
|
||||
}
|
||||
}
|
||||
9
packages/integrations/src/pi-hole/pi-hole-types.ts
Normal file
9
packages/integrations/src/pi-hole/pi-hole-types.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { z } from "@homarr/validation";
|
||||
|
||||
export const summaryResponseSchema = z.object({
|
||||
status: z.enum(["enabled", "disabled"]),
|
||||
domains_being_blocked: z.number(),
|
||||
ads_blocked_today: z.number(),
|
||||
dns_queries_today: z.number(),
|
||||
ads_percentage_today: z.number(),
|
||||
});
|
||||
1
packages/integrations/src/types.ts
Normal file
1
packages/integrations/src/types.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||
8
packages/integrations/tsconfig.json
Normal file
8
packages/integrations/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user