Pull request: 3972-hostlists-services

Updates #3972.

Squashed commit of the following:

commit 9dc0efe2453cb6c738d97d39b02c86eccb18a42c
Merge: 239550f8 8a935d4f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Oct 27 14:42:38 2022 +0300

    Merge branch 'master' into 3972-hostlists-services

commit 239550f84228e7c7a6f4ae6b1cadcc47e01f54d5
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Thu Oct 27 14:41:42 2022 +0300

    filtering: upd service list

commit b8bf3a6a4b1333059b886be95a1419612aebac39
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 27 13:41:09 2022 +0300

    client: remove todo

commit caa504b482befb804db2a1ca0b6d4834aa4da49a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 27 12:54:45 2022 +0300

    fix build

commit 511797c305d9eef84a20553dab795414e00da51a
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 27 12:40:33 2022 +0300

    client: add titles with service names to the clients table

commit 79ed3157a85b489a0b13381cff867a8c73ba60e9
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 27 12:36:59 2022 +0300

    client: fix empty icons

commit ab69b95784de87665d5a1a3683f28e3b3df1c210
Author: Ildar Kamalov <ik@adguard.com>
Date:   Thu Oct 27 11:55:48 2022 +0300

    client: use all blocked services

commit 9a4a87665c8463224d8e93f1e162988107f6c7ca
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 25 19:25:20 2022 +0300

    all: fix json response

commit 86eb4493ce305cd5991176bd4cd8f7f5afdea330
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Oct 25 19:09:44 2022 +0300

    all: use hostslists registry for blocked svcs
This commit is contained in:
Ainar Garipov 2022-10-27 15:46:25 +03:00
parent 8a935d4ffb
commit 9951d861d1
27 changed files with 987 additions and 922 deletions

View File

@ -17,6 +17,10 @@ and this project adheres to
## Added
- Several new blockable services ([#3972]). Those will now be more in sync with
the services that are already blockable in AdGuard DNS.
- A new HTTP API, `GET /control/blocked_services/all`, that lists all available
blocked services and their data, such as SVG icons ([#3972]).
- The new optional `tls.override_tls_ciphers` property, which allows
overriding TLS ciphers used by AdGuard Home ([#4925], [#4990]).
- The ability to serve DNS on link-local IPv6 addresses ([#2926]).
@ -28,6 +32,11 @@ and this project adheres to
- Responses with `SERVFAIL` code are now cached for at least 30 seconds.
### Deprecated
- The `GET /control/blocked_services/services` HTTP API; use the new
`GET /control/blocked_services/all` API instead ([#3972]).
### Fixed
- ClientIDs not working when using DNS-over-HTTPS with HTTP/3.
@ -41,6 +50,7 @@ and this project adheres to
[#2926]: https://github.com/AdguardTeam/AdGuardHome/issues/2926
[#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418
[#3972]: https://github.com/AdguardTeam/AdGuardHome/issues/3972
[#4916]: https://github.com/AdguardTeam/AdGuardHome/issues/4916
[#4925]: https://github.com/AdguardTeam/AdGuardHome/issues/4925
[#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942

View File

@ -32,6 +32,21 @@ export const getBlockedServices = () => async (dispatch) => {
}
};
export const getAllBlockedServicesRequest = createAction('GET_ALL_BLOCKED_SERVICES_REQUEST');
export const getAllBlockedServicesFailure = createAction('GET_ALL_BLOCKED_SERVICES_FAILURE');
export const getAllBlockedServicesSuccess = createAction('GET_ALL_BLOCKED_SERVICES_SUCCESS');
export const getAllBlockedServices = () => async (dispatch) => {
dispatch(getAllBlockedServicesRequest());
try {
const data = await apiClient.getAllBlockedServices();
dispatch(getAllBlockedServicesSuccess(data));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getAllBlockedServicesFailure());
}
};
export const setBlockedServicesRequest = createAction('SET_BLOCKED_SERVICES_REQUEST');
export const setBlockedServicesFailure = createAction('SET_BLOCKED_SERVICES_FAILURE');
export const setBlockedServicesSuccess = createAction('SET_BLOCKED_SERVICES_SUCCESS');

View File

@ -465,11 +465,18 @@ class Api {
BLOCKED_SERVICES_SET = { path: 'blocked_services/set', method: 'POST' };
BLOCKED_SERVICES_ALL = { path: 'blocked_services/all', method: 'GET' };
getBlockedServicesAvailableServices() {
const { path, method } = this.BLOCKED_SERVICES_SERVICES;
return this.makeRequest(path, method);
}
getAllBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_ALL;
return this.makeRequest(path, method);
}
getBlockedServices() {
const { path, method } = this.BLOCKED_SERVICES_LIST;
return this.makeRequest(path, method);

View File

@ -6,10 +6,11 @@ import flow from 'lodash/flow';
import { toggleAllServices } from '../../../helpers/helpers';
import { renderServiceField } from '../../../helpers/form';
import { FORM_NAME, SERVICES } from '../../../helpers/constants';
import { FORM_NAME } from '../../../helpers/constants';
const Form = (props) => {
const {
blockedServices,
handleSubmit,
change,
pristine,
@ -27,7 +28,7 @@ const Form = (props) => {
type="button"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
onClick={() => toggleAllServices(SERVICES, change, true)}
onClick={() => toggleAllServices(blockedServices, change, true)}
>
<Trans>block_all</Trans>
</button>
@ -37,17 +38,17 @@ const Form = (props) => {
type="button"
className="btn btn-secondary btn-block"
disabled={processing || processingSet}
onClick={() => toggleAllServices(SERVICES, change, false)}
onClick={() => toggleAllServices(blockedServices, change, false)}
>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
<div className="services">
{SERVICES.map((service) => (
{blockedServices.map((service) => (
<Field
key={service.id}
icon={`service_${service.id}`}
icon={service.icon_svg}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
@ -72,6 +73,7 @@ const Form = (props) => {
};
Form.propTypes = {
blockedServices: PropTypes.array.isRequired,
pristine: PropTypes.bool.isRequired,
handleSubmit: PropTypes.func.isRequired,
change: PropTypes.func.isRequired,

View File

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import Form from './Form';
import Card from '../../ui/Card';
import { getBlockedServices, setBlockedServices } from '../../../actions/services';
import { getBlockedServices, getAllBlockedServices, setBlockedServices } from '../../../actions/services';
import PageTitle from '../../ui/PageTitle';
const getInitialDataForServices = (initial) => (initial ? initial.reduce(
@ -21,6 +21,7 @@ const Services = () => {
useEffect(() => {
dispatch(getBlockedServices());
dispatch(getAllBlockedServices());
}, []);
const handleSubmit = (values) => {
@ -49,6 +50,7 @@ const Services = () => {
<div className="form">
<Form
initialValues={initialValues}
blockedServices={services.allServices}
processing={services.processing}
processingSet={services.processingSet}
onSubmit={handleSubmit}

View File

@ -27,6 +27,7 @@ const ResponseCell = ({
const filters = useSelector((state) => state.filtering.filters, shallowEqual);
const whitelistFilters = useSelector((state) => state.filtering.whitelistFilters, shallowEqual);
const isDetailed = useSelector((state) => state.queryLogs.isDetailed);
const services = useSelector((store) => store?.services);
const formattedElapsedMs = formatElapsedMs(elapsedMs, t);
@ -60,8 +61,8 @@ const ResponseCell = ({
install_settings_dns: upstreamString,
elapsed: formattedElapsedMs,
response_code: status,
...(service_name
&& { service_name: getServiceName(service_name) }
...(service_name && services.allServices
&& { service_name: getServiceName(services.allServices, service_name) }
),
...(rules.length > 0
&& { rule_label: getRulesToFilterList(rules, filters, whitelistFilters) }
@ -80,10 +81,10 @@ const ResponseCell = ({
const getDetailedInfo = (reason) => {
switch (reason) {
case FILTERED_STATUS.FILTERED_BLOCKED_SERVICE:
if (!service_name) {
if (!service_name || !services.allServices) {
return formattedElapsedMs;
}
return getServiceName(service_name);
return getServiceName(services.allServices, service_name);
case FILTERED_STATUS.FILTERED_BLACK_LIST:
case FILTERED_STATUS.NOT_FILTERED_WHITE_LIST:
return getFilterNames(rules, filters, whitelistFilters).join(', ');

View File

@ -52,6 +52,7 @@ const Row = memo(({
const autoClients = useSelector((state) => state.dashboard.autoClients, shallowEqual);
const processingSet = useSelector((state) => state.access.processingSet);
const allowedСlients = useSelector((state) => state.access.allowed_clients, shallowEqual);
const services = useSelector((store) => store?.services);
const clients = useSelector((state) => state.dashboard.clients);
@ -175,8 +176,8 @@ const Row = memo(({
date: formatDateTime(time, DEFAULT_SHORT_DATE_FORMAT_OPTIONS),
encryption_status: isBlocked
? <div className="bg--danger">{requestStatus}</div> : requestStatus,
...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE && service_name
&& { service_name: getServiceName(service_name) }),
...(FILTERED_STATUS.FILTERED_BLOCKED_SERVICE && service_name && services.allServices
&& { service_name: getServiceName(services.allServices, service_name) }),
domain,
type_table_header: type,
protocol,

View File

@ -16,6 +16,7 @@ import { getFilteringStatus } from '../../actions/filtering';
import { getClients } from '../../actions';
import { getDnsConfig } from '../../actions/dnsConfig';
import { getAccessList } from '../../actions/access';
import { getAllBlockedServices } from '../../actions/services';
import {
getLogsConfig,
resetFilteredLogs,
@ -130,6 +131,7 @@ const Logs = () => {
setIsLoading(true);
dispatch(getFilteringStatus());
dispatch(getClients());
dispatch(getAllBlockedServices());
try {
await Promise.all([
dispatch(getLogsConfig()),

View File

@ -1,25 +1,57 @@
import React, { Component, Fragment } from 'react';
/* eslint-disable react/display-name */
/* eslint-disable react/prop-types */
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { Trans, withTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import ReactTable from 'react-table';
import { MODAL_TYPE } from '../../../helpers/constants';
import { splitByNewLine, countClientsStatistics, sortIp } from '../../../helpers/helpers';
import Card from '../../ui/Card';
import Modal from './Modal';
import CellWrap from '../../ui/CellWrap';
import LogsSearchLink from '../../ui/LogsSearchLink';
import { getAllBlockedServices } from '../../../../actions/services';
import {
splitByNewLine,
countClientsStatistics,
sortIp,
getService,
} from '../../../../helpers/helpers';
import { MODAL_TYPE } from '../../../../helpers/constants';
import Card from '../../../ui/Card';
import CellWrap from '../../../ui/CellWrap';
import LogsSearchLink from '../../../ui/LogsSearchLink';
import Modal from '../Modal';
class ClientsTable extends Component {
handleFormAdd = (values) => {
this.props.addClient(values);
const ClientsTable = ({
clients,
normalizedTopClients,
isModalOpen,
modalClientName,
modalType,
addClient,
updateClient,
deleteClient,
toggleClientModal,
processingAdding,
processingDeleting,
processingUpdating,
getStats,
supportedTags,
}) => {
const [t] = useTranslation();
const dispatch = useDispatch();
const services = useSelector((store) => store?.services);
useEffect(() => {
dispatch(getAllBlockedServices());
}, []);
const handleFormAdd = (values) => {
addClient(values);
};
handleFormUpdate = (values, name) => {
this.props.updateClient(values, name);
const handleFormUpdate = (values, name) => {
updateClient(values, name);
};
handleSubmit = (values) => {
const handleSubmit = (values) => {
const config = values;
if (values) {
@ -42,21 +74,21 @@ class ClientsTable extends Component {
}
}
if (this.props.modalType === MODAL_TYPE.EDIT_FILTERS) {
this.handleFormUpdate(config, this.props.modalClientName);
if (modalType === MODAL_TYPE.EDIT_FILTERS) {
handleFormUpdate(config, modalClientName);
} else {
this.handleFormAdd(config);
handleFormAdd(config);
}
};
getOptionsWithLabels = (options) => (
const getOptionsWithLabels = (options) => (
options.map((option) => ({
value: option,
label: option,
}))
);
getClient = (name, clients) => {
const getClient = (name, clients) => {
const client = clients.find((item) => name === item.name);
if (client) {
@ -65,7 +97,7 @@ class ClientsTable extends Component {
} = client;
return {
upstreams: (upstreams && upstreams.join('\n')) || '',
tags: (tags && this.getOptionsWithLabels(tags)) || [],
tags: (tags && getOptionsWithLabels(tags)) || [],
...values,
};
}
@ -78,17 +110,17 @@ class ClientsTable extends Component {
};
};
handleDelete = (data) => {
const handleDelete = (data) => {
// eslint-disable-next-line no-alert
if (window.confirm(this.props.t('client_confirm_delete', { key: data.name }))) {
this.props.deleteClient(data);
this.props.getStats();
if (window.confirm(t('client_confirm_delete', { key: data.name }))) {
deleteClient(data);
getStats();
}
};
columns = [
const columns = [
{
Header: this.props.t('table_client'),
Header: t('table_client'),
accessor: 'ids',
minWidth: 150,
Cell: (row) => {
@ -109,13 +141,13 @@ class ClientsTable extends Component {
sortMethod: sortIp,
},
{
Header: this.props.t('table_name'),
Header: t('table_name'),
accessor: 'name',
minWidth: 120,
Cell: CellWrap,
},
{
Header: this.props.t('settings'),
Header: t('settings'),
accessor: 'use_global_settings',
minWidth: 120,
Cell: ({ value }) => {
@ -133,7 +165,7 @@ class ClientsTable extends Component {
},
},
{
Header: this.props.t('blocked_services'),
Header: t('blocked_services'),
accessor: 'blocked_services',
minWidth: 180,
Cell: (row) => {
@ -143,25 +175,40 @@ class ClientsTable extends Component {
return <Trans>settings_global</Trans>;
}
if (value && services.allServices) {
return (
<div className="logs__row logs__row--icons">
{value.map((service) => {
const serviceInfo = getService(services.allServices, service);
if (serviceInfo?.icon_svg) {
return (
<div
key={serviceInfo.name}
dangerouslySetInnerHTML={{
__html: window.atob(serviceInfo.icon_svg),
}}
className="service__icon service__icon--table"
title={serviceInfo.name}
/>
);
}
return null;
})}
</div>
);
}
return (
<div className="logs__row logs__row--icons">
{value && value.length > 0
? value.map((service) => (
<svg
className="service__icon service__icon--table"
title={service}
key={service}
>
<use xlinkHref={`#service_${service}`} />
</svg>
))
: ''}
</div>
);
},
},
{
Header: this.props.t('upstreams'),
Header: t('upstreams'),
accessor: 'upstreams',
minWidth: 120,
Cell: ({ value }) => {
@ -179,7 +226,7 @@ class ClientsTable extends Component {
},
},
{
Header: this.props.t('tags_title'),
Header: t('tags_title'),
accessor: 'tags',
minWidth: 140,
Cell: (row) => {
@ -203,11 +250,11 @@ class ClientsTable extends Component {
},
},
{
Header: this.props.t('requests_count'),
Header: t('requests_count'),
id: 'statistics',
accessor: (row) => countClientsStatistics(
row.ids,
this.props.normalizedTopClients.auto,
normalizedTopClients.auto,
),
sortMethod: (a, b) => b - a,
minWidth: 120,
@ -222,16 +269,13 @@ class ClientsTable extends Component {
},
},
{
Header: this.props.t('actions_table_header'),
Header: t('actions_table_header'),
accessor: 'actions',
maxWidth: 100,
sortable: false,
resizable: false,
Cell: (row) => {
const clientName = row.original.name;
const {
toggleClientModal, processingDeleting, processingUpdating, t,
} = this.props;
return (
<div className="logs__row logs__row--center">
@ -253,7 +297,7 @@ class ClientsTable extends Component {
<button
type="button"
className="btn btn-icon btn-outline-secondary btn-sm"
onClick={() => this.handleDelete({ name: clientName })}
onClick={() => handleDelete({ name: clientName })}
disabled={processingDeleting}
title={t('delete_table_action')}
>
@ -267,76 +311,61 @@ class ClientsTable extends Component {
},
];
render() {
const {
t,
clients,
isModalOpen,
modalType,
modalClientName,
toggleClientModal,
processingAdding,
processingUpdating,
supportedTags,
} = this.props;
const currentClientData = getClient(modalClientName, clients);
const tagsOptions = getOptionsWithLabels(supportedTags);
const currentClientData = this.getClient(modalClientName, clients);
const tagsOptions = this.getOptionsWithLabels(supportedTags);
return (
<Card
title={t('clients_title')}
subtitle={t('clients_desc')}
bodyType="card-body box-body--settings"
>
<Fragment>
<ReactTable
data={clients || []}
columns={this.columns}
defaultSorted={[
{
id: 'statistics',
asc: true,
},
]}
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={10}
minRows={5}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}
pageText={t('page_table_footer_text')}
rowsText={t('rows_table_footer_text')}
loadingText={t('loading_table_status')}
noDataText={t('clients_not_found')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}
disabled={processingAdding}
>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
toggleClientModal={toggleClientModal}
currentClientData={currentClientData}
handleSubmit={this.handleSubmit}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
tagsOptions={tagsOptions}
/>
</Fragment>
</Card>
);
}
}
return (
<Card
title={t('clients_title')}
subtitle={t('clients_desc')}
bodyType="card-body box-body--settings"
>
<>
<ReactTable
data={clients || []}
columns={columns}
defaultSorted={[
{
id: 'statistics',
asc: true,
},
]}
className="-striped -highlight card-table-overflow"
showPagination
defaultPageSize={10}
minRows={5}
ofText="/"
previousText={t('previous_btn')}
nextText={t('next_btn')}
pageText={t('page_table_footer_text')}
rowsText={t('rows_table_footer_text')}
loadingText={t('loading_table_status')}
noDataText={t('clients_not_found')}
/>
<button
type="button"
className="btn btn-success btn-standard mt-3"
onClick={() => toggleClientModal(MODAL_TYPE.ADD_FILTERS)}
disabled={processingAdding}
>
<Trans>client_add</Trans>
</button>
<Modal
isModalOpen={isModalOpen}
modalType={modalType}
toggleClientModal={toggleClientModal}
currentClientData={currentClientData}
handleSubmit={handleSubmit}
processingAdding={processingAdding}
processingUpdating={processingUpdating}
tagsOptions={tagsOptions}
/>
</>
</Card>
);
};
ClientsTable.propTypes = {
t: PropTypes.func.isRequired,
clients: PropTypes.array.isRequired,
normalizedTopClients: PropTypes.object.isRequired,
toggleClientModal: PropTypes.func.isRequired,
@ -353,4 +382,4 @@ ClientsTable.propTypes = {
supportedTags: PropTypes.array.isRequired,
};
export default withTranslation()(ClientsTable);
export default ClientsTable;

View File

@ -0,0 +1 @@
export { default as ClientsTable } from './ClientsTable';

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { connect, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
Field, FieldArray, reduxForm, formValueSelector,
@ -19,7 +19,7 @@ import {
renderServiceField,
} from '../../../helpers/form';
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
import { CLIENT_ID_LINK, FORM_NAME, SERVICES } from '../../../helpers/constants';
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
import './Service.css';
const settingsCheckboxes = [
@ -139,6 +139,7 @@ let Form = (props) => {
invalid,
tagsOptions,
} = props;
const services = useSelector((store) => store?.services);
const [activeTabLabel, setActiveTabLabel] = useState('settings');
@ -180,7 +181,9 @@ let Form = (props) => {
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => toggleAllServices(SERVICES, change, true)}
onClick={() => (
toggleAllServices(services.allServices, change, true)
)}
>
<Trans>block_all</Trans>
</button>
@ -190,25 +193,29 @@ let Form = (props) => {
type="button"
className="btn btn-secondary btn-block"
disabled={useGlobalServices}
onClick={() => toggleAllServices(SERVICES, change, false)}
onClick={() => (
toggleAllServices(services.allServices, change, false)
)}
>
<Trans>unblock_all</Trans>
</button>
</div>
</div>
<div className="services">
{SERVICES.map((service) => (
<Field
key={service.id}
icon={`service_${service.id}`}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
placeholder={service.name}
disabled={useGlobalServices}
/>
))}
</div>
{services.allServices.length > 0 && (
<div className="services">
{services.allServices.map((service) => (
<Field
key={service.id}
icon={service.icon_svg}
name={`blocked_services.${service.id}`}
type="checkbox"
component={renderServiceField}
placeholder={service.name}
disabled={useGlobalServices}
/>
))}
</div>
)}
</div>
</div>,
},

View File

@ -9,6 +9,12 @@
cursor: pointer;
}
.service__text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@media screen and (min-width: 768px) {
.services {
display: flex;
@ -33,7 +39,7 @@
margin-right: 30px;
margin-left: 0;
}
.service:nth-child(3n) {
margin-right: 0;
margin-left: auto;

View File

@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react';
import { withTranslation } from 'react-i18next';
import PropTypes from 'prop-types';
import ClientsTable from './ClientsTable';
import { ClientsTable } from './ClientsTable';
import AutoClients from './AutoClients';
import PageTitle from '../../ui/PageTitle';
import Loading from '../../ui/Loading';

File diff suppressed because one or more lines are too long

View File

@ -15452,6 +15452,7 @@ a.tag-addon:hover {
}
.custom-switch-indicator {
flex-shrink: 0;
display: inline-block;
height: 1.25rem;
width: 2.25rem;

View File

@ -202,158 +202,6 @@ export const FILTERS_URLS = {
blocked_services: '/blocked_services',
};
export const SERVICES = [
{
id: '9gag',
name: '9GAG',
},
{
id: 'amazon',
name: 'Amazon',
},
{
id: 'bilibili',
name: 'Bilibili',
},
{
id: 'cloudflare',
name: 'CloudFlare',
},
{
id: 'dailymotion',
name: 'Dailymotion',
},
{
id: 'discord',
name: 'Discord',
},
{
id: 'disneyplus',
name: 'Disney+',
},
{
id: 'ebay',
name: 'EBay',
},
{
id: 'epic_games',
name: 'Epic Games',
},
{
id: 'facebook',
name: 'Facebook',
},
{
id: 'hulu',
name: 'Hulu',
},
{
id: 'imgur',
name: 'Imgur',
},
{
id: 'instagram',
name: 'Instagram',
},
{
id: 'mail_ru',
name: 'Mail.ru',
},
{
id: 'netflix',
name: 'Netflix',
},
{
id: 'ok',
name: 'OK.ru',
},
{
id: 'origin',
name: 'Origin',
},
{
id: 'pinterest',
name: 'Pinterest',
},
{
id: 'qq',
name: 'QQ',
},
{
id: 'reddit',
name: 'Reddit',
},
{
id: 'skype',
name: 'Skype',
},
{
id: 'snapchat',
name: 'Snapchat',
},
{
id: 'spotify',
name: 'Spotify',
},
{
id: 'steam',
name: 'Steam',
},
{
id: 'telegram',
name: 'Telegram',
},
{
id: 'tiktok',
name: 'TikTok',
},
{
id: 'tinder',
name: 'Tinder',
},
{
id: 'twitch',
name: 'Twitch',
},
{
id: 'twitter',
name: 'Twitter',
},
{
id: 'viber',
name: 'Viber',
},
{
id: 'vimeo',
name: 'Vimeo',
},
{
id: 'vk',
name: 'VK.com',
},
{
id: 'wechat',
name: 'WeChat',
},
{
id: 'weibo',
name: 'Weibo',
},
{
id: 'whatsapp',
name: 'WhatsApp',
},
{
id: 'youtube',
name: 'YouTube',
},
];
export const SERVICES_ID_NAME_MAP = SERVICES.reduce((acc, { id, name }) => {
acc[id] = name;
return acc;
}, {});
export const ENCRYPTION_SOURCE = {
PATH: 'path',
CONTENT: 'content',

View File

@ -1,6 +1,8 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Trans } from 'react-i18next';
import cn from 'classnames';
import { createOnBlurHandler } from './helpers';
import { R_MAC_WITHOUT_COLON, R_UNIX_ABSOLUTE_PATH, R_WIN_ABSOLUTE_PATH } from './constants';
@ -229,24 +231,34 @@ export const renderServiceField = ({
modifier,
icon,
meta: { touched, error },
}) => <Fragment>
<label className={`service custom-switch ${modifier}`}>
<input
{...input}
type="checkbox"
className="custom-switch-input"
value={placeholder.toLowerCase()}
disabled={disabled}
/>
<span className="service__switch custom-switch-indicator"></span>
<span className="service__text">{placeholder}</span>
<svg className="service__icon">
<use xlinkHref={`#${icon}`} />
</svg>
</label>
{!disabled && touched && error
&& <span className="form__message form__message--error"><Trans>{error}</Trans></span>}
</Fragment>;
}) => (
<>
<label className={cn('service custom-switch', { [modifier]: modifier })}>
<input
{...input}
type="checkbox"
className="custom-switch-input"
value={placeholder.toLowerCase()}
disabled={disabled}
/>
<span className="service__switch custom-switch-indicator"></span>
<span className="service__text" title={placeholder}>
{placeholder}
</span>
{icon && (
<div
dangerouslySetInnerHTML={{ __html: window.atob(icon) }}
className="service__icon"
/>
)}
</label>
{!disabled && touched && error && (
<span className="form__message form__message--error">
<Trans>{error}</Trans>
</span>
)}
</>
);
renderServiceField.propTypes = {
input: PropTypes.object.isRequired,

View File

@ -21,7 +21,6 @@ import {
FILTERED,
FILTERED_STATUS,
R_CLIENT_ID,
SERVICES_ID_NAME_MAP,
STANDARD_DNS_PORT,
STANDARD_HTTPS_PORT,
STANDARD_WEB_PORT,
@ -991,7 +990,22 @@ export const filterOutComments = (lines) => lines
.filter((line) => !line.startsWith(COMMENT_LINE_DEFAULT_TOKEN));
/**
* @param {string} serviceId
* @param {array} services
* @param {string} id
* @returns {string}
*/
export const getServiceName = (serviceId) => SERVICES_ID_NAME_MAP[serviceId] || serviceId;
export const getService = (services, id) => services.find((s) => s.id === id);
/**
* @param {array} services
* @param {string} id
* @returns {string}
*/
export const getServiceName = (services, id) => getService(services, id)?.name;
/**
* @param {array} services
* @param {string} id
* @returns {string}
*/
export const getServiceIcon = (services, id) => getService(services, id)?.icon_svg;

View File

@ -12,6 +12,14 @@ const services = handleActions(
processing: false,
}),
[actions.getAllBlockedServicesRequest]: (state) => ({ ...state, processingAll: true }),
[actions.getAllBlockedServicesFailure]: (state) => ({ ...state, processingAll: false }),
[actions.getAllBlockedServicesSuccess]: (state, { payload }) => ({
...state,
allServices: payload.blocked_services,
processingAll: false,
}),
[actions.setBlockedServicesRequest]: (state) => ({ ...state, processingSet: true }),
[actions.setBlockedServicesFailure]: (state) => ({ ...state, processingSet: false }),
[actions.setBlockedServicesSuccess]: (state) => ({
@ -21,8 +29,10 @@ const services = handleActions(
},
{
processing: true,
processingAll: true,
processingSet: false,
list: [],
allServices: [],
},
);

View File

@ -10,382 +10,6 @@ import (
"golang.org/x/exp/slices"
)
// svc represents a single blocked service.
type svc struct {
name string
rules []string
}
// servicesData contains raw blocked service data.
//
// Keep in sync with:
// - client/src/helpers/constants.js
// - client/src/components/ui/Icons.js
var servicesData = []svc{{
name: "whatsapp",
rules: []string{
"||wa.me^",
"||whatsapp.com^",
"||whatsapp.net^",
},
}, {
name: "facebook",
rules: []string{
"||facebook.com^",
"||facebook.net^",
"||fbcdn.net^",
"||accountkit.com^",
"||fb.me^",
"||fb.com^",
"||fb.gg^",
"||fbsbx.com^",
"||fbwat.ch^",
"||messenger.com^",
"||facebookcorewwwi.onion^",
"||fbcdn.com^",
"||fb.watch^",
},
}, {
name: "twitter",
rules: []string{
"||t.co^",
"||twimg.com^",
"||twitter.com^",
"||twttr.com^",
},
}, {
name: "youtube",
rules: []string{
"||googlevideo.com^",
"||wide-youtube.l.google.com^",
"||youtu.be^",
"||youtube",
"||youtube-nocookie.com^",
"||youtube.com^",
"||youtubei.googleapis.com^",
"||youtubekids.com^",
"||ytimg.com^",
},
}, {
name: "twitch",
rules: []string{
"||jtvnw.net^",
"||ttvnw.net^",
"||twitch.tv^",
"||twitchcdn.net^",
},
}, {
name: "netflix",
rules: []string{
"||nflxext.com^",
"||netflix.com^",
"||nflximg.net^",
"||nflxvideo.net^",
"||nflxso.net^",
},
}, {
name: "instagram",
rules: []string{"||instagram.com^", "||cdninstagram.com^"},
}, {
name: "snapchat",
rules: []string{
"||snapchat.com^",
"||sc-cdn.net^",
"||snap-dev.net^",
"||snapkit.co",
"||snapads.com^",
"||impala-media-production.s3.amazonaws.com^",
},
}, {
name: "discord",
rules: []string{
"||discord.gg^",
"||discordapp.net^",
"||discordapp.com^",
"||discord.com^",
"||discord.gift",
"||discord.media^",
},
}, {
name: "ok",
rules: []string{"||ok.ru^"},
}, {
name: "skype",
rules: []string{
"||edge-skype-com.s-0001.s-msedge.net^",
"||skype-edf.akadns.net^",
"||skype.com^",
"||skypeassets.com^",
"||skypedata.akadns.net^",
},
}, {
name: "vk",
rules: []string{
"||userapi.com^",
"||vk-cdn.net^",
"||vk.com^",
"||vkuservideo.net^",
},
}, {
name: "origin",
rules: []string{
"||accounts.ea.com^",
"||origin.com^",
"||signin.ea.com^",
},
}, {
name: "steam",
rules: []string{
"||steam.com^",
"||steampowered.com^",
"||steamcommunity.com^",
"||steamstatic.com^",
"||steamstore-a.akamaihd.net^",
"||steamcdn-a.akamaihd.net^",
},
}, {
name: "epic_games",
rules: []string{"||epicgames.com^", "||easyanticheat.net^", "||easy.ac^", "||eac-cdn.com^"},
}, {
name: "reddit",
rules: []string{"||reddit.com^", "||redditstatic.com^", "||redditmedia.com^", "||redd.it^"},
}, {
name: "mail_ru",
rules: []string{"||mail.ru^"},
}, {
name: "cloudflare",
rules: []string{
"||1.1.1.1^",
"||argotunnel.com^",
"||cloudflare-dns.com^",
"||cloudflare-ipfs.com^",
"||cloudflare-quic.com^",
"||cloudflare.cn^",
"||cloudflare.com^",
"||cloudflare.net^",
"||cloudflareaccess.com^",
"||cloudflareapps.com^",
"||cloudflarebolt.com^",
"||cloudflareclient.com^",
"||cloudflareinsights.com^",
"||cloudflareresolve.com^",
"||cloudflarestatus.com^",
"||cloudflarestream.com^",
"||cloudflarewarp.com^",
"||dns4torpnlfs2ifuz2s2yf3fc7rdmsbhm6rw75euj35pac6ap25zgqad.onion^",
"||one.one^",
"||pages.dev^",
"||trycloudflare.com^",
"||videodelivery.net^",
"||warp.plus^",
"||workers.dev^",
},
}, {
name: "amazon",
rules: []string{
"||amazon.com^",
"||media-amazon.com^",
"||primevideo.com^",
"||amazontrust.com^",
"||images-amazon.com^",
"||ssl-images-amazon.com^",
"||amazonpay.com^",
"||amazonpay.in^",
"||amazon-adsystem.com^",
"||a2z.com^",
"||amazon.ae^",
"||amazon.ca^",
"||amazon.cn^",
"||amazon.de^",
"||amazon.es^",
"||amazon.fr^",
"||amazon.in^",
"||amazon.it^",
"||amazon.nl^",
"||amazon.com.au^",
"||amazon.com.br^",
"||amazon.co.jp^",
"||amazon.com.mx^",
"||amazon.com.tr^",
"||amazon.co.uk^",
"||createspace.com^",
"||aws",
},
}, {
name: "ebay",
rules: []string{
"||ebay.com^",
"||ebayimg.com^",
"||ebaystatic.com^",
"||ebaycdn.net^",
"||ebayinc.com^",
"||ebay.at^",
"||ebay.be^",
"||ebay.ca^",
"||ebay.ch^",
"||ebay.cn^",
"||ebay.de^",
"||ebay.es^",
"||ebay.fr^",
"||ebay.ie^",
"||ebay.in^",
"||ebay.it^",
"||ebay.ph^",
"||ebay.pl^",
"||ebay.nl^",
"||ebay.com.au^",
"||ebay.com.cn^",
"||ebay.com.hk^",
"||ebay.com.my^",
"||ebay.com.sg^",
"||ebay.co.uk^",
},
}, {
name: "tiktok",
rules: []string{
"||amemv.com^",
"||bdurl.com^",
"||bytecdn.cn^",
"||bytedance.map.fastly.net^",
"||bytedapm.com^",
"||byteimg.com^",
"||byteoversea.com^",
"||douyin.com^",
"||douyincdn.com^",
"||douyinpic.com^",
"||douyinstatic.com^",
"||douyinvod.com^",
"||ixigua.com^",
"||ixiguavideo.com^",
"||muscdn.com^",
"||musical.ly^",
"||pstatp.com^",
"||snssdk.com^",
"||tiktok.com^",
"||tiktokcdn.com^",
"||tiktokv.com^",
"||toutiao.com^",
"||toutiaocloud.com^",
"||toutiaocloud.net^",
"||toutiaovod.com^",
},
}, {
name: "vimeo",
rules: []string{
"*vod-adaptive.akamaized.net^",
"||vimeo.com^",
"||vimeocdn.com^",
},
}, {
name: "pinterest",
rules: []string{
"||pinimg.com^",
"||pinterest.*^",
},
}, {
name: "imgur",
rules: []string{"||imgur.com^"},
}, {
name: "dailymotion",
rules: []string{
"||dailymotion.com^",
"||dm-event.net^",
"||dmcdn.net^",
},
}, {
name: "qq",
rules: []string{
// Block qq.com and subdomains excluding WeChat's domains.
"||qq.com^$denyallow=wx.qq.com|weixin.qq.com",
"||qqzaixian.com^",
"||qq-video.cdn-go.cn^",
"||url.cn^",
},
}, {
name: "wechat",
rules: []string{
"||wechat.com^",
"||weixin.qq.com.cn^",
"||weixin.qq.com^",
"||weixinbridge.com^",
"||wx.qq.com^",
},
}, {
name: "viber",
rules: []string{"||viber.com^"},
}, {
name: "weibo",
rules: []string{
"||weibo.cn^",
"||weibo.com^",
"||weibocdn.com^",
},
}, {
name: "9gag",
rules: []string{
"||9cache.com^",
"||9gag.com^",
},
}, {
name: "telegram",
rules: []string{
"||t.me^",
"||telegram.me^",
"||telegram.org^",
},
}, {
name: "disneyplus",
rules: []string{
"||disney-plus.net^",
"||disney.playback.edge.bamgrid.com^",
"||disneynow.com^",
"||disneyplus.com^",
"||hotstar.com^",
"||media.dssott.com^",
"||star.playback.edge.bamgrid.com^",
"||starplus.com^",
},
}, {
name: "hulu",
rules: []string{"||hulu.com^"},
}, {
name: "spotify",
rules: []string{
"/_spotify-connect._tcp.local/",
"||spotify.com^",
"||scdn.co^",
"||spotify.com.edgesuite.net^",
"||spotify.map.fastly.net^",
"||spotify.map.fastlylb.net^",
"||spotifycdn.net^",
"||audio-ak-spotify-com.akamaized.net^",
"||audio4-ak-spotify-com.akamaized.net^",
"||heads-ak-spotify-com.akamaized.net^",
"||heads4-ak-spotify-com.akamaized.net^",
},
}, {
name: "tinder",
rules: []string{
"||gotinder.com^",
"||tinder.com^",
"||tindersparks.com^",
},
}, {
name: "bilibili",
rules: []string{
"||b23.tv^",
"||biliapi.net^",
"||bilibili.com^",
"||bilicdn1.com^",
"||bilicdn2.com^",
"||biligame.com^",
"||bilivideo.cn^",
"||bilivideo.com^",
"||dreamcast.hk^",
"||hdslb.com^",
},
}}
// serviceRules maps a service ID to its filtering rules.
var serviceRules map[string][]*rules.NetworkRule
@ -394,16 +18,16 @@ var serviceIDs []string
// initBlockedServices initializes package-level blocked service data.
func initBlockedServices() {
l := len(servicesData)
l := len(blockedServices)
serviceIDs = make([]string, l)
serviceRules = make(map[string][]*rules.NetworkRule, l)
for i, s := range servicesData {
netRules := make([]*rules.NetworkRule, 0, len(s.rules))
for _, text := range s.rules {
for i, s := range blockedServices {
netRules := make([]*rules.NetworkRule, 0, len(s.Rules))
for _, text := range s.Rules {
rule, err := rules.NewNetworkRule(text, BlockedSvcsListID)
if err != nil {
log.Error("parsing blocked service %q rule %q: %s", s.name, text, err)
log.Error("parsing blocked service %q rule %q: %s", s.ID, text, err)
continue
}
@ -411,8 +35,8 @@ func initBlockedServices() {
netRules = append(netRules, rule)
}
serviceIDs[i] = s.name
serviceRules[s.name] = netRules
serviceIDs[i] = s.ID
serviceRules[s.ID] = netRules
}
slices.Sort(serviceIDs)
@ -420,7 +44,7 @@ func initBlockedServices() {
log.Debug("filtering: initialized %d services", l)
}
// BlockedSvcKnown - return TRUE if a blocked service name is known
// BlockedSvcKnown returns true if a blocked service ID is known.
func BlockedSvcKnown(s string) (ok bool) {
_, ok = serviceRules[s]
@ -452,10 +76,18 @@ func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
}
}
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
func (d *DNSFilter) handleBlockedServicesIDs(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
}
func (d *DNSFilter) handleBlockedServicesAll(w http.ResponseWriter, r *http.Request) {
_ = aghhttp.WriteJSONResponse(w, r, struct {
BlockedServices []blockedService `json:"blocked_services"`
}{
BlockedServices: blockedServices,
})
}
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
d.confLock.RLock()
list := d.Config.BlockedServices
@ -479,5 +111,5 @@ func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Requ
log.Debug("Updated blocked services list: %d", len(list))
d.ConfigModified()
d.Config.ConfigModified()
}

View File

@ -466,7 +466,8 @@ func (d *DNSFilter) RegisterFilteringHandlers() {
registerHTTP(http.MethodPost, "/control/rewrite/add", d.handleRewriteAdd)
registerHTTP(http.MethodPost, "/control/rewrite/delete", d.handleRewriteDelete)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesAvailableServices)
registerHTTP(http.MethodGet, "/control/blocked_services/services", d.handleBlockedServicesIDs)
registerHTTP(http.MethodGet, "/control/blocked_services/all", d.handleBlockedServicesAll)
registerHTTP(http.MethodGet, "/control/blocked_services/list", d.handleBlockedServicesList)
registerHTTP(http.MethodPost, "/control/blocked_services/set", d.handleBlockedServicesSet)

File diff suppressed because one or more lines are too long

View File

@ -6,6 +6,19 @@
## v0.107.17: API Changes
### `GET /control/blocked_services/services` is deprecated
Use `GET /control/blocked_services/all`.
### `GET /control/blocked_services/all`
* The new `GET /control/blocked_services/all` HTTP API allows inspecting all
available services and their data, such as SVG icons and human-readable names.
## v0.107.15: `POST` Requests Without Bodies
As an additional CSRF protection measure, AdGuard Home now ensures that requests

View File

@ -862,6 +862,9 @@
- 'clients'
'/blocked_services/services':
'get':
'deprecated': true
'description': >
Deprecated: Use `GET /blocked_services/all` instead.
'tags':
- 'blocked_services'
'operationId': 'blockedServicesAvailableServices'
@ -873,6 +876,19 @@
'application/json':
'schema':
'$ref': '#/components/schemas/BlockedServicesArray'
'/blocked_services/all':
'get':
'tags':
- 'blocked_services'
'operationId': 'blockedServicesAll'
'summary': 'Get available services to use for blocking'
'responses':
'200':
'description': 'OK.'
'content':
'application/json':
'schema':
'$ref': '#/components/schemas/BlockedServicesAll'
'/blocked_services/list':
'get':
'tags':
@ -2539,6 +2555,42 @@
'type': 'array'
'items':
'type': 'string'
'BlockedServicesAll':
'properties':
'blocked_services':
'items':
'$ref': '#/components/schemas/BlockedService'
'type': 'array'
'required':
- 'blocked_services'
'type': 'object'
'BlockedService':
'properties':
'icon_svg':
'description': >
The SVG icon as a Base64-encoded string to make it easier to embed
it into a data URL.
'type': 'string'
'id':
'description': >
The ID of this service.
'type': 'string'
'name':
'description': >
The human-readable name of this service.
'type': 'string'
'rules':
'description': >
The array of the filtering rules.
'items':
'type': 'string'
'type': 'array'
'required':
- 'icon_svg'
- 'id'
- 'name'
- 'rules'
'type': 'object'
'CheckConfigRequestBeta':
'type': 'object'
'description': 'Configuration to be checked'

View File

@ -208,10 +208,30 @@ code from [the repo][companiesrepo].
## `blocked-services/`: Blocked Services Updater
A simple script that downloads and updates the blocked services index from
AdGuard's [Hostlists Registry][reg].
Optional environment:
* `URL`: the URL of the index file. By default it's
`https://adguardteam.github.io/HostlistsRegistry/assets/services.json`.
### Usage
```sh
go run ./scripts/blocked-services/main.go
```
[reg]: https://github.com/AdguardTeam/HostlistsRegistry
## `vetted-filters/`: Vetted Filters Updater
A simple script that downloads and updates the vetted filtering list data from
AdGuard's [Hostlists Registry][reg].
Similar to the one above, a script that downloads and updates the vetted
filtering list data from AdGuard's [Hostlists Registry][reg].
Optional environment:
@ -223,5 +243,3 @@ Optional environment:
```sh
go run ./scripts/vetted-filters/main.go
```
[reg]: https://github.com/AdguardTeam/HostlistsRegistry

View File

@ -0,0 +1,117 @@
// blocked services fetches the most recent Hostlists Registry blocked service
// index and transforms the filters from it to AdGuard Home's data and code
// formats.
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"text/template"
"time"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/exp/slices"
)
func main() {
urlStr := "https://adguardteam.github.io/HostlistsRegistry/assets/services.json"
if v, ok := os.LookupEnv("URL"); ok {
urlStr = v
}
// Validate the URL.
_, err := url.Parse(urlStr)
check(err)
c := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := c.Get(urlStr)
check(err)
defer log.OnCloserError(resp.Body, log.ERROR)
if resp.StatusCode != http.StatusOK {
panic(fmt.Errorf("expected code %d, got %d", http.StatusOK, resp.StatusCode))
}
hlSvcs := &hlServices{}
err = json.NewDecoder(resp.Body).Decode(hlSvcs)
check(err)
// Sort all services and rules to make the output more predictable.
slices.SortStableFunc(hlSvcs.BlockedServices, func(a, b *hlServicesService) (less bool) {
return a.ID < b.ID
})
for _, s := range hlSvcs.BlockedServices {
slices.Sort(s.Rules)
}
// Use another set of delimiters to prevent them interfering with the Go
// code.
tmpl, err := template.New("main").Delims("<%", "%>").Funcs(template.FuncMap{
"isnotlast": func(idx, sliceLen int) (ok bool) { return idx != sliceLen-1 },
}).Parse(tmplStr)
check(err)
f, err := os.OpenFile(
"./internal/filtering/servicelist.go",
os.O_CREATE|os.O_TRUNC|os.O_WRONLY,
0o644,
)
check(err)
defer log.OnCloserError(f, log.ERROR)
err = tmpl.Execute(f, hlSvcs)
check(err)
}
// tmplStr is the template for the Go source file with the services.
const tmplStr = `// Code generated by go run ./scripts/blocked-services/main.go; DO NOT EDIT.
package filtering
// blockedService represents a single blocked service.
type blockedService struct {
ID string ` + "`" + `json:"id"` + "`" + `
Name string ` + "`" + `json:"name"` + "`" + `
IconSVG []byte ` + "`" + `json:"icon_svg"` + "`" + `
Rules []string ` + "`" + `json:"rules"` + "`" + `
}
// blockedServices contains raw blocked service data.
var blockedServices = []blockedService{<% $l := len .BlockedServices %>
<%- range $i, $s := .BlockedServices %>{
ID: <% printf "%q" $s.ID %>,
Name: <% printf "%q" $s.Name %>,
IconSVG: []byte(<% printf "%q" $s.IconSVG %>),
Rules: []string{<% range $s.Rules %>
<% printf "%q" . %>,<% end %>
},
}<% if isnotlast $i $l %>, <% end %><% end %>}
`
// check is a simple error-checking helper for scripts.
func check(err error) {
if err != nil {
panic(err)
}
}
// hlServices is the JSON structure for the Hostlists Registry blocked service
// index.
type hlServices struct {
BlockedServices []*hlServicesService `json:"blocked_services"`
}
// hlServicesService is the JSON structure for a service in the Hostlists
// Registry.
type hlServicesService struct {
ID string `json:"id"`
Name string `json:"name"`
IconSVG string `json:"icon_svg"`
Rules []string `json:"rules"`
}

View File

@ -1,5 +1,5 @@
// vetted-filters fetches the most recent Hostlists Registry index and
// transforms the filters from it to AdGuard Home's format.
// vetted-filters fetches the most recent Hostlists Registry filtering rule list
// index and transforms the filters from it to AdGuard Home's format.
package main
import (
@ -91,7 +91,7 @@ func main() {
check(err)
}
// jsHeader is the header for the generates JavaScript file. It informs the
// jsHeader is the header for the generated JavaScript file. It informs the
// reader that the file is generated and disables some style-related eslint
// checks.
const jsHeader = `// Code generated by go run ./scripts/vetted-filters/main.go; DO NOT EDIT.