mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-16 10:16:20 +01:00
✨ Add ad guard home (#937)
* ✨ Add add guard home * ✨ Add request for blocked domains and fix request for blocked queries * ♻️ PR feedback * ✅ Fix tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { Group, Select, SelectItem, Text } from '@mantine/core';
|
||||
import { Group, Image, Select, SelectItem, Text } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { forwardRef } from 'react';
|
||||
@@ -87,9 +87,14 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||
},
|
||||
{
|
||||
value: 'pihole',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pihole.png',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/pi-hole.png',
|
||||
label: 'PiHole',
|
||||
},
|
||||
{
|
||||
value: 'adGuardHome',
|
||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/adguard-home.png',
|
||||
label: 'AdGuard Home',
|
||||
},
|
||||
].filter((x) => Object.keys(integrationFieldProperties).includes(x.value));
|
||||
|
||||
const getNewProperties = (value: string | null): AppIntegrationPropertyType[] => {
|
||||
@@ -133,11 +138,12 @@ export const IntegrationSelector = ({ form }: IntegrationSelectorProps) => {
|
||||
}
|
||||
icon={
|
||||
form.values.integration?.type && (
|
||||
<img
|
||||
<Image
|
||||
src={data.find((x) => x.value === form.values.integration?.type)?.image}
|
||||
alt="integration"
|
||||
width={20}
|
||||
height={20}
|
||||
fit="contain"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -160,7 +166,7 @@ const SelectItemComponent = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ image, label, description, ...others }: ItemProps, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<img src={image} alt="integration icon" width={20} height={20} />
|
||||
<Image src={image} alt="integration icon" width={20} height={20} fit="contain" />
|
||||
|
||||
<div>
|
||||
<Text size="sm">{label}</Text>
|
||||
|
||||
@@ -5,6 +5,8 @@ import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { findAppProperty } from '../../../../tools/client/app-properties';
|
||||
import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
|
||||
import { ConfigAppType } from '../../../../types/app';
|
||||
import { AdGuard } from '../../../../tools/server/sdk/adGuard/adGuard';
|
||||
|
||||
const getQuerySchema = z.object({
|
||||
status: z.enum(['enabled', 'disabled']),
|
||||
@@ -21,29 +23,50 @@ export const Post = async (request: NextApiRequest, response: NextApiResponse) =
|
||||
return;
|
||||
}
|
||||
|
||||
const applicableApps = config.apps.filter((x) => x.integration?.type === 'pihole');
|
||||
const applicableApps = config.apps.filter(
|
||||
(x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type)
|
||||
);
|
||||
|
||||
for (let i = 0; i < applicableApps.length; i += 1) {
|
||||
const app = applicableApps[i];
|
||||
|
||||
const pihole = new PiHoleClient(
|
||||
app.url,
|
||||
findAppProperty(app, 'password')
|
||||
);
|
||||
|
||||
switch (parseResult.data.status) {
|
||||
case 'enabled':
|
||||
await pihole.enable();
|
||||
break;
|
||||
case 'disabled':
|
||||
await pihole.disable();
|
||||
break;
|
||||
if (app.integration?.type === 'pihole') {
|
||||
await processPiHole(app, parseResult.data.status === 'disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
await processAdGuard(app, parseResult.data.status === 'disabled');
|
||||
}
|
||||
|
||||
response.status(200).json({});
|
||||
};
|
||||
|
||||
const processAdGuard = async (app: ConfigAppType, enable: boolean) => {
|
||||
const adGuard = new AdGuard(
|
||||
app.url,
|
||||
findAppProperty(app, 'username'),
|
||||
findAppProperty(app, 'password')
|
||||
);
|
||||
|
||||
if (enable) {
|
||||
await adGuard.disable();
|
||||
return;
|
||||
}
|
||||
|
||||
await adGuard.enable();
|
||||
};
|
||||
|
||||
const processPiHole = async (app: ConfigAppType, enable: boolean) => {
|
||||
const pihole = new PiHoleClient(app.url, findAppProperty(app, 'password'));
|
||||
|
||||
if (enable) {
|
||||
await pihole.enable();
|
||||
return;
|
||||
}
|
||||
|
||||
await pihole.disable();
|
||||
};
|
||||
|
||||
export default async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
if (request.method === 'POST') {
|
||||
return Post(request, response);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import Consola from 'consola';
|
||||
import { getCookie } from 'cookies-next';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
@@ -5,12 +6,15 @@ import { findAppProperty } from '../../../../tools/client/app-properties';
|
||||
import { getConfig } from '../../../../tools/config/getConfig';
|
||||
import { PiHoleClient } from '../../../../tools/server/sdk/pihole/piHole';
|
||||
import { AdStatistics } from '../../../../widgets/dnshole/type';
|
||||
import { AdGuard } from '../../../../tools/server/sdk/adGuard/adGuard';
|
||||
|
||||
export const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
const configName = getCookie('config-name', { req: request });
|
||||
const config = getConfig(configName?.toString() ?? 'default');
|
||||
|
||||
const applicableApps = config.apps.filter((x) => x.integration?.type === 'pihole');
|
||||
const applicableApps = config.apps.filter(
|
||||
(x) => x.integration?.type && ['pihole', 'adGuardHome'].includes(x.integration?.type)
|
||||
);
|
||||
|
||||
const data: AdStatistics = {
|
||||
domainsBeingBlocked: 0,
|
||||
@@ -26,21 +30,51 @@ export const Get = async (request: NextApiRequest, response: NextApiResponse) =>
|
||||
const app = applicableApps[i];
|
||||
|
||||
try {
|
||||
const piHole = new PiHoleClient(app.url, findAppProperty(app, 'password'));
|
||||
switch (app.integration?.type) {
|
||||
case 'pihole': {
|
||||
const piHole = new PiHoleClient(app.url, findAppProperty(app, 'password'));
|
||||
const summary = await piHole.getSummary();
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const summary = await piHole.getSummary();
|
||||
data.domainsBeingBlocked += summary.domains_being_blocked;
|
||||
data.adsBlockedToday += summary.ads_blocked_today;
|
||||
data.dnsQueriesToday += summary.dns_queries_today;
|
||||
data.status.push({
|
||||
status: summary.status,
|
||||
appId: app.id,
|
||||
});
|
||||
adsBlockedTodayPercentageArr.push(summary.ads_percentage_today);
|
||||
break;
|
||||
}
|
||||
case 'adGuardHome': {
|
||||
const adGuard = new AdGuard(
|
||||
app.url,
|
||||
findAppProperty(app, 'username'),
|
||||
findAppProperty(app, 'password')
|
||||
);
|
||||
|
||||
data.domainsBeingBlocked += summary.domains_being_blocked;
|
||||
data.adsBlockedToday += summary.ads_blocked_today;
|
||||
data.dnsQueriesToday += summary.dns_queries_today;
|
||||
data.status.push({
|
||||
status: summary.status,
|
||||
appId: app.id,
|
||||
});
|
||||
adsBlockedTodayPercentageArr.push(summary.ads_percentage_today);
|
||||
const stats = await adGuard.getStats();
|
||||
const status = await adGuard.getStatus();
|
||||
const countFilteredDomains = await adGuard.getCountFilteringDomains();
|
||||
|
||||
const blockedQueriesToday = stats.blocked_filtering.reduce((prev, sum) => prev + sum, 0);
|
||||
const queriesToday = stats.dns_queries.reduce((prev, sum) => prev + sum, 0);
|
||||
data.adsBlockedToday = blockedQueriesToday;
|
||||
data.domainsBeingBlocked += countFilteredDomains;
|
||||
data.dnsQueriesToday += queriesToday;
|
||||
data.status.push({
|
||||
status: status.protection_enabled ? 'enabled' : 'disabled',
|
||||
appId: app.id,
|
||||
});
|
||||
adsBlockedTodayPercentageArr.push((queriesToday / blockedQueriesToday) * 100);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
Consola.error(`Integration communication for app ${app.id} failed: unknown type`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Consola.error(`Failed to communicate with PiHole at ${app.url}: ${err}`);
|
||||
Consola.error(`Failed to communicate with DNS hole at ${app.url}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('media-requests api', () => {
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetch and return requests in response', async () => {
|
||||
it('fetch and return requests in response with external url', async () => {
|
||||
// Arrange
|
||||
const { req, res } = createMocks({
|
||||
method: 'GET',
|
||||
@@ -108,7 +108,16 @@ describe('media-requests api', () => {
|
||||
},
|
||||
},
|
||||
],
|
||||
} as ConfigType);
|
||||
widgets: [
|
||||
{
|
||||
id: 'hjeruijgrig',
|
||||
type: 'media-requests-list',
|
||||
properties: {
|
||||
replaceLinksWithExternalHost: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as ConfigType);
|
||||
const logSpy = vi.spyOn(Consola, 'error');
|
||||
|
||||
fetchMock.mockResponse((request) => {
|
||||
@@ -287,6 +296,235 @@ describe('media-requests api', () => {
|
||||
'https://image.tmdb.org/t/p/w600_and_h900_bestv2//hf4j0928gq543njgh8935nqh8.jpg',
|
||||
status: 2,
|
||||
type: 'movie',
|
||||
userLink: 'http://my-overseerr.external/users/1',
|
||||
userName: 'Example User',
|
||||
userProfilePicture: 'http://my-overseerr.external//os_logo_square.png',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(logSpy).not.toHaveBeenCalled();
|
||||
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('fetch and return requests in response with internal url', async () => {
|
||||
// Arrange
|
||||
const { req, res } = createMocks({
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
vi.mock('./../../../../tools/config/getConfig.ts', () => ({
|
||||
get getConfig() {
|
||||
return mockedGetConfig;
|
||||
},
|
||||
}));
|
||||
mockedGetConfig.mockReturnValue({
|
||||
apps: [
|
||||
{
|
||||
url: 'http://my-overseerr.local',
|
||||
behaviour: {
|
||||
externalUrl: 'http://my-overseerr.external',
|
||||
},
|
||||
integration: {
|
||||
type: 'overseerr',
|
||||
properties: [
|
||||
{
|
||||
field: 'apiKey',
|
||||
type: 'private',
|
||||
value: 'abc',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
widgets: [
|
||||
{
|
||||
id: 'hjeruijgrig',
|
||||
type: 'media-requests-list',
|
||||
properties: {
|
||||
replaceLinksWithExternalHost: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as ConfigType);
|
||||
const logSpy = vi.spyOn(Consola, 'error');
|
||||
|
||||
fetchMock.mockResponse((request) => {
|
||||
if (request.url === 'http://my-overseerr.local/api/v1/request?take=25&skip=0&sort=added') {
|
||||
return JSON.stringify({
|
||||
pageInfo: { pages: 3, pageSize: 20, results: 42, page: 1 },
|
||||
results: [
|
||||
{
|
||||
id: 44,
|
||||
status: 2,
|
||||
createdAt: '2023-04-06T19:38:45.000Z',
|
||||
updatedAt: '2023-04-06T19:38:45.000Z',
|
||||
type: 'movie',
|
||||
is4k: false,
|
||||
serverId: 0,
|
||||
profileId: 4,
|
||||
tags: [],
|
||||
isAutoRequest: false,
|
||||
media: {
|
||||
downloadStatus: [],
|
||||
downloadStatus4k: [],
|
||||
id: 999,
|
||||
mediaType: 'movie',
|
||||
tmdbId: 99999999,
|
||||
tvdbId: null,
|
||||
imdbId: null,
|
||||
status: 5,
|
||||
status4k: 1,
|
||||
createdAt: '2023-02-06T19:38:45.000Z',
|
||||
updatedAt: '2023-02-06T20:00:04.000Z',
|
||||
lastSeasonChange: '2023-08-06T19:38:45.000Z',
|
||||
mediaAddedAt: '2023-05-14T06:30:34.000Z',
|
||||
serviceId: 0,
|
||||
serviceId4k: null,
|
||||
externalServiceId: 32,
|
||||
externalServiceId4k: null,
|
||||
externalServiceSlug: '000000000000',
|
||||
externalServiceSlug4k: null,
|
||||
ratingKey: null,
|
||||
ratingKey4k: null,
|
||||
jellyfinMediaId: '0000',
|
||||
jellyfinMediaId4k: null,
|
||||
mediaUrl:
|
||||
'http://your-jellyfin.local/web/index.html#!/details?id=mn8q2j4gq038g&context=home&serverId=jf83fj34gm340g',
|
||||
serviceUrl: 'http://your-jellyfin.local/movie/0000',
|
||||
},
|
||||
seasons: [],
|
||||
modifiedBy: {
|
||||
permissions: 2,
|
||||
warnings: [],
|
||||
id: 1,
|
||||
email: 'example-user@homarr.dev',
|
||||
plexUsername: null,
|
||||
jellyfinUsername: 'example-user',
|
||||
username: null,
|
||||
recoveryLinkExpirationDate: null,
|
||||
userType: 3,
|
||||
plexId: null,
|
||||
jellyfinUserId: '00000000000000000',
|
||||
jellyfinDeviceId: '111111111111111111',
|
||||
jellyfinAuthToken: '2222222222222222222',
|
||||
plexToken: null,
|
||||
avatar: '/os_logo_square.png',
|
||||
movieQuotaLimit: null,
|
||||
movieQuotaDays: null,
|
||||
tvQuotaLimit: null,
|
||||
tvQuotaDays: null,
|
||||
createdAt: '2022-07-03T19:53:08.000Z',
|
||||
updatedAt: '2022-07-03T19:53:08.000Z',
|
||||
requestCount: 34,
|
||||
displayName: 'Example User',
|
||||
},
|
||||
requestedBy: {
|
||||
permissions: 2,
|
||||
warnings: [],
|
||||
id: 1,
|
||||
email: 'example-user@homarr.dev',
|
||||
plexUsername: null,
|
||||
jellyfinUsername: 'example-user',
|
||||
username: null,
|
||||
recoveryLinkExpirationDate: null,
|
||||
userType: 3,
|
||||
plexId: null,
|
||||
jellyfinUserId: '00000000000000000',
|
||||
jellyfinDeviceId: '111111111111111111',
|
||||
jellyfinAuthToken: '2222222222222222222',
|
||||
plexToken: null,
|
||||
avatar: '/os_logo_square.png',
|
||||
movieQuotaLimit: null,
|
||||
movieQuotaDays: null,
|
||||
tvQuotaLimit: null,
|
||||
tvQuotaDays: null,
|
||||
createdAt: '2022-07-03T19:53:08.000Z',
|
||||
updatedAt: '2022-07-03T19:53:08.000Z',
|
||||
requestCount: 34,
|
||||
displayName: 'Example User',
|
||||
},
|
||||
seasonCount: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (request.url === 'http://my-overseerr.local/api/v1/movie/99999999') {
|
||||
return JSON.stringify({
|
||||
id: 0,
|
||||
adult: false,
|
||||
budget: 0,
|
||||
genres: [
|
||||
{
|
||||
id: 18,
|
||||
name: 'Dashboards',
|
||||
},
|
||||
],
|
||||
relatedVideos: [],
|
||||
originalLanguage: 'jp',
|
||||
originalTitle: 'Homarrrr Movie',
|
||||
popularity: 9.352,
|
||||
productionCompanies: [],
|
||||
productionCountries: [],
|
||||
releaseDate: '2023-12-08',
|
||||
releases: {
|
||||
results: [],
|
||||
},
|
||||
revenue: 0,
|
||||
spokenLanguages: [
|
||||
{
|
||||
english_name: 'Japanese',
|
||||
iso_639_1: 'jp',
|
||||
name: '日本語',
|
||||
},
|
||||
],
|
||||
status: 'Released',
|
||||
title: 'Homarr Movie',
|
||||
video: false,
|
||||
voteAverage: 9.999,
|
||||
voteCount: 0,
|
||||
backdropPath: '/mhjq8jr0qgrjnghnh.jpg',
|
||||
homepage: '',
|
||||
imdbId: 'tt0000000',
|
||||
overview: 'A very cool movie',
|
||||
posterPath: '/hf4j0928gq543njgh8935nqh8.jpg',
|
||||
runtime: 97,
|
||||
tagline: '',
|
||||
credits: {},
|
||||
collection: null,
|
||||
externalIds: {
|
||||
facebookId: null,
|
||||
imdbId: null,
|
||||
instagramId: null,
|
||||
twitterId: null,
|
||||
},
|
||||
watchProviders: [],
|
||||
keywords: [],
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Bad url: ${request.url}`));
|
||||
});
|
||||
|
||||
// Act
|
||||
await MediaRequestsRoute(req, res);
|
||||
|
||||
// Assert
|
||||
expect(res._getStatusCode()).toBe(200);
|
||||
expect(res.finished).toBe(true);
|
||||
expect(JSON.parse(res._getData())).toEqual([
|
||||
{
|
||||
airDate: '2023-12-08',
|
||||
backdropPath: 'https://image.tmdb.org/t/p/original//mhjq8jr0qgrjnghnh.jpg',
|
||||
createdAt: '2023-04-06T19:38:45.000Z',
|
||||
href: 'http://my-overseerr.local/movie/99999999',
|
||||
id: 44,
|
||||
name: 'Homarrrr Movie',
|
||||
posterPath:
|
||||
'https://image.tmdb.org/t/p/w600_and_h900_bestv2//hf4j0928gq543njgh8935nqh8.jpg',
|
||||
status: 2,
|
||||
type: 'movie',
|
||||
userLink: 'http://my-overseerr.local/users/1',
|
||||
userName: 'Example User',
|
||||
userProfilePicture: 'http://my-overseerr.local//os_logo_square.png',
|
||||
|
||||
@@ -51,7 +51,7 @@ const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
type: item.type,
|
||||
name: genericItem.name,
|
||||
userName: item.requestedBy.displayName,
|
||||
userProfilePicture: constructAvatarUrl(app, item),
|
||||
userProfilePicture: constructAvatarUrl(appUrl, item),
|
||||
userLink: `${appUrl}/users/${item.requestedBy.id}`,
|
||||
airDate: genericItem.airDate,
|
||||
status: item.status,
|
||||
@@ -75,7 +75,7 @@ const Get = async (request: NextApiRequest, response: NextApiResponse) => {
|
||||
return response.status(200).json(mediaRequests);
|
||||
};
|
||||
|
||||
const constructAvatarUrl = (app: ConfigAppType, item: OverseerrResponseItem) => {
|
||||
const constructAvatarUrl = (appUrl: string, item: OverseerrResponseItem) => {
|
||||
const isAbsolute =
|
||||
item.requestedBy.avatar.startsWith('http://') || item.requestedBy.avatar.startsWith('https://');
|
||||
|
||||
@@ -83,7 +83,7 @@ const constructAvatarUrl = (app: ConfigAppType, item: OverseerrResponseItem) =>
|
||||
return item.requestedBy.avatar;
|
||||
}
|
||||
|
||||
return `${app.url}/${item.requestedBy.avatar}`;
|
||||
return `${appUrl}/${item.requestedBy.avatar}`;
|
||||
};
|
||||
|
||||
const retrieveDetailsForItem = async (
|
||||
|
||||
41
src/tools/server/sdk/adGuard/adGuard.schema.ts
Normal file
41
src/tools/server/sdk/adGuard/adGuard.schema.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const adGuardApiStatsResponseSchema = z.object({
|
||||
time_units: z.enum(['hours']),
|
||||
top_queried_domains: z.array(z.record(z.string(), z.number())),
|
||||
top_clients: z.array(z.record(z.string(), z.number())),
|
||||
top_blocked_domains: z.array(z.record(z.string(), z.number())),
|
||||
dns_queries: z.array(z.number()),
|
||||
blocked_filtering: z.array(z.number()),
|
||||
replaced_safebrowsing: z.array(z.number()),
|
||||
replaced_parental: z.array(z.number()),
|
||||
num_dns_queries: z.number().min(0),
|
||||
num_blocked_filtering: z.number().min(0),
|
||||
num_replaced_safebrowsing: z.number().min(0),
|
||||
num_replaced_safesearch: z.number().min(0),
|
||||
num_replaced_parental: z.number().min(0),
|
||||
avg_processing_time: z.number().min(0).max(1),
|
||||
});
|
||||
|
||||
export const adGuardApiStatusResponseSchema = z.object({
|
||||
version: z.string(),
|
||||
language: z.string(),
|
||||
dns_addresses: z.array(z.string()),
|
||||
dns_port: z.number().positive(),
|
||||
http_port: z.number().positive(),
|
||||
protection_disabled_duration: z.number(),
|
||||
protection_enabled: z.boolean(),
|
||||
dhcp_available: z.boolean(),
|
||||
running: z.boolean(),
|
||||
});
|
||||
|
||||
export const adGuardApiFilteringStatusSchema = z.object({
|
||||
filters: z.array(z.object({
|
||||
url: z.string().url(),
|
||||
name: z.string(),
|
||||
last_updated: z.string().optional(),
|
||||
id: z.number().nonnegative(),
|
||||
rules_count: z.number().nonnegative(),
|
||||
enabled: z.boolean(),
|
||||
})),
|
||||
});
|
||||
95
src/tools/server/sdk/adGuard/adGuard.ts
Normal file
95
src/tools/server/sdk/adGuard/adGuard.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { trimStringEnding } from '../../../shared/strings';
|
||||
import {
|
||||
adGuardApiFilteringStatusSchema,
|
||||
adGuardApiStatsResponseSchema,
|
||||
adGuardApiStatusResponseSchema,
|
||||
} from './adGuard.schema';
|
||||
|
||||
export class AdGuard {
|
||||
private readonly baseHostName: string;
|
||||
|
||||
constructor(
|
||||
hostname: string,
|
||||
private readonly username: string,
|
||||
private readonly password: string
|
||||
) {
|
||||
this.baseHostName = trimStringEnding(hostname, ['/#', '/']);
|
||||
}
|
||||
|
||||
async getStats(): Promise<AdGuardStatsType> {
|
||||
const response = await fetch(`${this.baseHostName}/control/stats`, {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return adGuardApiStatsResponseSchema.parseAsync(data);
|
||||
}
|
||||
|
||||
async getStatus() {
|
||||
const response = await fetch(`${this.baseHostName}/control/status`, {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return adGuardApiStatusResponseSchema.parseAsync(data);
|
||||
}
|
||||
|
||||
async getCountFilteringDomains() {
|
||||
const response = await fetch(`${this.baseHostName}/control/filtering/status`, {
|
||||
headers: {
|
||||
Authorization: `Basic ${this.getAuthorizationHeaderValue()}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const schemaData = await adGuardApiFilteringStatusSchema.parseAsync(data);
|
||||
|
||||
return schemaData.filters
|
||||
.filter((filter) => filter.enabled)
|
||||
.reduce((sum, filter) => filter.rules_count + sum, 0);
|
||||
}
|
||||
|
||||
async disable() {
|
||||
await this.changeProtectionStatus(false);
|
||||
}
|
||||
async enable() {
|
||||
await this.changeProtectionStatus(false);
|
||||
}
|
||||
|
||||
private async changeProtectionStatus(newStatus: boolean, duration = 0) {
|
||||
await fetch(`${this.baseHostName}/control/protection`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
enabled: newStatus,
|
||||
duration,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private getAuthorizationHeaderValue() {
|
||||
return Buffer.from(`${this.username}:${this.password}`).toString('base64');
|
||||
}
|
||||
}
|
||||
|
||||
export type AdGuardStatsType = {
|
||||
time_units: string;
|
||||
top_queried_domains: { [key: string]: number }[];
|
||||
top_clients: { [key: string]: number }[];
|
||||
top_blocked_domains: { [key: string]: number }[];
|
||||
dns_queries: number[];
|
||||
blocked_filtering: number[];
|
||||
replaced_safebrowsing: number[];
|
||||
replaced_parental: number[];
|
||||
num_dns_queries: number;
|
||||
num_blocked_filtering: number;
|
||||
num_replaced_safebrowsing: number;
|
||||
num_replaced_safesearch: number;
|
||||
num_replaced_parental: number;
|
||||
avg_processing_time: number;
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { trimStringEnding } from '../../../shared/strings';
|
||||
import { PiHoleApiStatusChangeResponse, PiHoleApiSummaryResponse } from './piHole.type';
|
||||
|
||||
export class PiHoleClient {
|
||||
private readonly baseHostName: string;
|
||||
|
||||
constructor(hostname: string, private readonly apiToken: string) {
|
||||
this.baseHostName = this.trimStringEnding(hostname, ['/admin/index.php', '/admin', '/']);
|
||||
this.baseHostName = trimStringEnding(hostname, ['/admin/index.php', '/admin', '/']);
|
||||
}
|
||||
|
||||
async getSummary() {
|
||||
@@ -60,15 +61,4 @@ export class PiHoleClient {
|
||||
|
||||
return json as PiHoleApiStatusChangeResponse;
|
||||
}
|
||||
|
||||
private trimStringEnding(original: string, toTrimIfExists: string[]) {
|
||||
for (let i = 0; i < toTrimIfExists.length; i += 1) {
|
||||
if (!original.endsWith(toTrimIfExists[i])) {
|
||||
continue;
|
||||
}
|
||||
return original.substring(0, original.indexOf(toTrimIfExists[i]));
|
||||
}
|
||||
|
||||
return original;
|
||||
}
|
||||
}
|
||||
|
||||
10
src/tools/shared/strings.ts
Normal file
10
src/tools/shared/strings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const trimStringEnding = (original: string, toTrimIfExists: string[]) => {
|
||||
for (let i = 0; i < toTrimIfExists.length; i += 1) {
|
||||
if (!original.endsWith(toTrimIfExists[i])) {
|
||||
continue;
|
||||
}
|
||||
return original.substring(0, original.indexOf(toTrimIfExists[i]));
|
||||
}
|
||||
|
||||
return original;
|
||||
};
|
||||
@@ -45,7 +45,8 @@ export type IntegrationType =
|
||||
| 'plex'
|
||||
| 'jellyfin'
|
||||
| 'nzbGet'
|
||||
| 'pihole';
|
||||
| 'pihole'
|
||||
| 'adGuardHome';
|
||||
|
||||
export type AppIntegrationType = {
|
||||
type: IntegrationType | null;
|
||||
@@ -86,6 +87,7 @@ export const integrationFieldProperties: {
|
||||
jellyfin: ['username', 'password'],
|
||||
plex: ['apiKey'],
|
||||
pihole: ['password'],
|
||||
adGuardHome: ['username', 'password'],
|
||||
};
|
||||
|
||||
export type IntegrationFieldDefinitionType = {
|
||||
|
||||
@@ -136,7 +136,7 @@ function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
|
||||
<Stack align="center" spacing="xs">
|
||||
<IconSearch size={30} />
|
||||
<div>
|
||||
<Text align="center">{formatNumber(data.dnsQueriesToday, 0)}</Text>
|
||||
<Text align="center">{formatNumber(data.dnsQueriesToday, 3)}</Text>
|
||||
<Text align="center" lh={1.2} size="sm">
|
||||
{t('card.metrics.queriesToday')}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user