pl-fe: support configuring redirect services, i think it works but idk
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -55,7 +55,7 @@
|
|||||||
"@lexical/selection": "^0.29.0",
|
"@lexical/selection": "^0.29.0",
|
||||||
"@lexical/utils": "^0.29.0",
|
"@lexical/utils": "^0.29.0",
|
||||||
"@mkljczk/react-hotkeys": "^1.3.0",
|
"@mkljczk/react-hotkeys": "^1.3.0",
|
||||||
"@mkljczk/url-purify": "^0.0.2",
|
"@mkljczk/url-purify": "^0.0.3",
|
||||||
"@reach/combobox": "^0.18.0",
|
"@reach/combobox": "^0.18.0",
|
||||||
"@reach/rect": "^0.18.0",
|
"@reach/rect": "^0.18.0",
|
||||||
"@reach/tabs": "^0.18.0",
|
"@reach/tabs": "^0.18.0",
|
||||||
@ -136,6 +136,7 @@
|
|||||||
"sass": "^1.86.3",
|
"sass": "^1.86.3",
|
||||||
"stringz": "^2.1.0",
|
"stringz": "^2.1.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
|
"use-mutative": "^1.2.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"valibot": "^1.0.0-beta.12",
|
"valibot": "^1.0.0-beta.12",
|
||||||
"zustand": "^5.0.3",
|
"zustand": "^5.0.3",
|
||||||
|
|||||||
@ -50,6 +50,8 @@ interface IParsedContent {
|
|||||||
emojis?: Array<CustomEmoji>;
|
emojis?: Array<CustomEmoji>;
|
||||||
/** Whether to call a function to remove tracking parameters from URLs. */
|
/** Whether to call a function to remove tracking parameters from URLs. */
|
||||||
cleanUrls?: boolean;
|
cleanUrls?: boolean;
|
||||||
|
/** Whether to call a function to redirect URLs to popular websites to privacy-respecting proxy services. */
|
||||||
|
redirectUrls?: boolean;
|
||||||
/** Whether to display link target domain when it's not part of the text. */
|
/** Whether to display link target domain when it's not part of the text. */
|
||||||
displayTargetHost?: boolean;
|
displayTargetHost?: boolean;
|
||||||
greentext?: boolean;
|
greentext?: boolean;
|
||||||
@ -100,6 +102,7 @@ function parseContent({
|
|||||||
hasQuote,
|
hasQuote,
|
||||||
emojis,
|
emojis,
|
||||||
cleanUrls = false,
|
cleanUrls = false,
|
||||||
|
redirectUrls = false,
|
||||||
displayTargetHost = true,
|
displayTargetHost = true,
|
||||||
greentext = false,
|
greentext = false,
|
||||||
speakAsCat = false,
|
speakAsCat = false,
|
||||||
@ -172,7 +175,7 @@ function parseContent({
|
|||||||
|
|
||||||
if (cleanUrls) {
|
if (cleanUrls) {
|
||||||
try {
|
try {
|
||||||
href = Purify.clearUrl(href);
|
href = Purify.clearUrl(href, cleanUrls, redirectUrls);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@ -275,9 +278,11 @@ function parseContent({
|
|||||||
const ParsedContent: React.FC<IParsedContent> = React.memo((props) => {
|
const ParsedContent: React.FC<IParsedContent> = React.memo((props) => {
|
||||||
const { urlPrivacy } = useSettings();
|
const { urlPrivacy } = useSettings();
|
||||||
|
|
||||||
if (props.cleanUrls === undefined) {
|
props = { ...props };
|
||||||
props = { ...props, cleanUrls: urlPrivacy.clearLinksInContent, displayTargetHost: urlPrivacy.displayTargetHost };
|
|
||||||
}
|
if (props.cleanUrls === undefined) props.cleanUrls = urlPrivacy.clearLinksInContent;
|
||||||
|
if (props.redirectUrls === undefined) props.redirectUrls = urlPrivacy.redirectLinksMode !== 'off';
|
||||||
|
if (props.displayTargetHost === undefined) props.displayTargetHost = urlPrivacy.displayTargetHost;
|
||||||
|
|
||||||
return parseContent(props, false);
|
return parseContent(props, false);
|
||||||
}, (prevProps, nextProps) => prevProps.html === nextProps.html);
|
}, (prevProps, nextProps) => prevProps.html === nextProps.html);
|
||||||
|
|||||||
@ -147,6 +147,7 @@ const StatusContent: React.FC<IStatusContent> = React.memo(({
|
|||||||
hasQuote: !!status.quote_id,
|
hasQuote: !!status.quote_id,
|
||||||
emojis: status.emojis,
|
emojis: status.emojis,
|
||||||
cleanUrls: urlPrivacy.clearLinksInContent,
|
cleanUrls: urlPrivacy.clearLinksInContent,
|
||||||
|
redirectUrls: urlPrivacy.redirectLinksMode !== 'off',
|
||||||
displayTargetHost: urlPrivacy.displayTargetHost,
|
displayTargetHost: urlPrivacy.displayTargetHost,
|
||||||
greentext,
|
greentext,
|
||||||
speakAsCat: status.account.speak_as_cat,
|
speakAsCat: status.account.speak_as_cat,
|
||||||
|
|||||||
@ -41,7 +41,7 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
|||||||
|
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const compareUrl = (url: string) => {
|
const compareUrl = (url: string) => {
|
||||||
const cleanUrl = Purify.clearUrl(url);
|
const cleanUrl = Purify.clearUrl(url, true, false);
|
||||||
return {
|
return {
|
||||||
originalUrl: url,
|
originalUrl: url,
|
||||||
cleanUrl,
|
cleanUrl,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
|
import { mappings } from '@mkljczk/url-purify';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
import { useMutative } from 'use-mutative';
|
||||||
|
|
||||||
import { changeSetting } from 'pl-fe/actions/settings';
|
import { changeSetting } from 'pl-fe/actions/settings';
|
||||||
import List, { ListItem } from 'pl-fe/components/list';
|
import List, { ListItem } from 'pl-fe/components/list';
|
||||||
@ -12,16 +14,27 @@ import FormGroup from 'pl-fe/components/ui/form-group';
|
|||||||
import Input from 'pl-fe/components/ui/input';
|
import Input from 'pl-fe/components/ui/input';
|
||||||
import Toggle from 'pl-fe/components/ui/toggle';
|
import Toggle from 'pl-fe/components/ui/toggle';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||||
|
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||||
|
import KVStore from 'pl-fe/storage/kv-store';
|
||||||
|
import { KVStoreRedirectServicesItem } from 'pl-fe/utils/url-purify';
|
||||||
|
|
||||||
|
import { SelectDropdown } from '../forms';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
urlPrivacy: { id: 'settings.url_privacy', defaultMessage: 'URL privacy' },
|
urlPrivacy: { id: 'settings.url_privacy', defaultMessage: 'URL privacy' },
|
||||||
rulesUrlPlaceholder: { id: 'url_privacy.rules_url.placeholder', defaultMessage: 'Rules URL' },
|
rulesUrlPlaceholder: { id: 'url_privacy.rules_url.placeholder', defaultMessage: 'Rules URL' },
|
||||||
hashUrlPlaceholder: { id: 'url_privacy.hash_url.placeholder', defaultMessage: 'Hash URL' },
|
hashUrlPlaceholder: { id: 'url_privacy.hash_url.placeholder', defaultMessage: 'Hash URL' },
|
||||||
|
redirectLinksModeOff: { id: 'url_privacy.redirect_links_mode.off', defaultMessage: 'Disabled' },
|
||||||
|
redirectLinksModeAuto: { id: 'url_privacy.redirect_links_mode.auto', defaultMessage: 'From URL' },
|
||||||
|
redirectLinksModeManual: { id: 'url_privacy.redirect_links_mode.manual', defaultMessage: 'Specify manually' },
|
||||||
|
redirectServicesUrlPlaceholder: { id: 'url_privacy.redirect_services_url.placeholder', defaultMessage: 'Rules URL' },
|
||||||
|
redirectServicePlaceholder: { id: 'url_privacy.redirect_services_url.placeholder', defaultMessage: 'eg. https://proxy.example.org' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const UrlPrivacy = () => {
|
const UrlPrivacy = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const me = useAppSelector((state) => state.me);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { urlPrivacy } = useSettings();
|
const { urlPrivacy } = useSettings();
|
||||||
@ -29,25 +42,62 @@ const UrlPrivacy = () => {
|
|||||||
const [displayTargetHost, setDisplayTargetHost] = useState(urlPrivacy.displayTargetHost);
|
const [displayTargetHost, setDisplayTargetHost] = useState(urlPrivacy.displayTargetHost);
|
||||||
const [clearLinksInCompose, setClearLinksInCompose] = useState(urlPrivacy.clearLinksInCompose);
|
const [clearLinksInCompose, setClearLinksInCompose] = useState(urlPrivacy.clearLinksInCompose);
|
||||||
const [clearLinksInContent, setClearLinksInContent] = useState(urlPrivacy.clearLinksInContent);
|
const [clearLinksInContent, setClearLinksInContent] = useState(urlPrivacy.clearLinksInContent);
|
||||||
// const [allowReferralMarketing, setAllowReferralMarketing] = useState(urlPrivacy.allowReferralMarketing);
|
|
||||||
const [hashUrl, setHashUrl] = useState(urlPrivacy.hashUrl);
|
const [hashUrl, setHashUrl] = useState(urlPrivacy.hashUrl);
|
||||||
const [rulesUrl, setRulesUrl] = useState(urlPrivacy.rulesUrl);
|
const [rulesUrl, setRulesUrl] = useState(urlPrivacy.rulesUrl);
|
||||||
|
const [redirectLinksMode, setRedirectLinksMode] = useState(urlPrivacy.redirectLinksMode);
|
||||||
|
const [redirectServicesUrl, setRedirectServicesUrl] = useState(urlPrivacy.redirectServicesUrl);
|
||||||
|
const [redirectServices, setRedirectServices] = useMutative(urlPrivacy.redirectServices);
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
dispatch(changeSetting(['urlPrivacy'], {
|
const value = {
|
||||||
...urlPrivacy,
|
...urlPrivacy,
|
||||||
displayTargetHost,
|
displayTargetHost,
|
||||||
clearLinksInCompose,
|
clearLinksInCompose,
|
||||||
clearLinksInContent,
|
clearLinksInContent,
|
||||||
// allowReferralMarketing,
|
|
||||||
hashUrl,
|
hashUrl,
|
||||||
rulesUrl,
|
rulesUrl,
|
||||||
}, {
|
redirectLinksMode,
|
||||||
|
redirectServicesUrl,
|
||||||
|
redirectServices,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (redirectLinksMode) {
|
||||||
|
case 'off':
|
||||||
|
value.redirectServicesUrl = '';
|
||||||
|
value.redirectServices = {};
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
value.redirectServicesUrl = '';
|
||||||
|
break;
|
||||||
|
case 'auto':
|
||||||
|
value.redirectServices = {};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(changeSetting(['urlPrivacy'], value, {
|
||||||
save: true,
|
save: true,
|
||||||
showAlert: true,
|
showAlert: true,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeRedirectLinksMode = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
if (redirectLinksMode === 'auto' && event.target.value === 'manual') {
|
||||||
|
KVStore.getItem<KVStoreRedirectServicesItem>(`url-purify-redirect-services:${me}`).then((services) => {
|
||||||
|
if (!services?.redirectServices) return;
|
||||||
|
|
||||||
|
setRedirectServices(
|
||||||
|
Object.fromEntries(
|
||||||
|
mappings.map(({ name, targets }) => ([
|
||||||
|
name,
|
||||||
|
services.redirectServices.find((service) => targets.includes(service.type) && service.instances.length)?.instances[0].split('|')[0] || '',
|
||||||
|
])),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).catch(() => { });
|
||||||
|
}
|
||||||
|
return setRedirectLinksMode(event.target.value as 'off');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
@ -74,10 +124,6 @@ const UrlPrivacy = () => {
|
|||||||
<ListItem label={<FormattedMessage id='url_privacy.clear_links_in_content' defaultMessage='Remove tracking parameters from displayed posts' />}>
|
<ListItem label={<FormattedMessage id='url_privacy.clear_links_in_content' defaultMessage='Remove tracking parameters from displayed posts' />}>
|
||||||
<Toggle checked={clearLinksInContent} onChange={({ target }) => setClearLinksInContent(target.checked)} />
|
<Toggle checked={clearLinksInContent} onChange={({ target }) => setClearLinksInContent(target.checked)} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{/* <ListItem label={<FormattedMessage id='url_privacy.allow_referral_marketing' defaultMessage='Make exception for referral marketing parameters' />}>
|
|
||||||
<Toggle checked={allowReferralMarketing} onChange={({ target }) => setAllowReferralMarketing(target.checked)} disabled={!clearLinksInCompose && !clearLinksInContent} />
|
|
||||||
</ListItem> */}
|
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
@ -104,6 +150,55 @@ const UrlPrivacy = () => {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<List>
|
||||||
|
<ListItem label={<FormattedMessage id='url_privacy.redirect_links_mode' defaultMessage='Redirect links to popular websites to privacy-respecting proxy services' />}>
|
||||||
|
<SelectDropdown
|
||||||
|
className='max-w-fit'
|
||||||
|
items={{
|
||||||
|
off: intl.formatMessage(messages.redirectLinksModeOff),
|
||||||
|
auto: intl.formatMessage(messages.redirectLinksModeAuto),
|
||||||
|
manual: intl.formatMessage(messages.redirectLinksModeManual),
|
||||||
|
}}
|
||||||
|
defaultValue={redirectLinksMode}
|
||||||
|
onChange={handleChangeRedirectLinksMode}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{redirectLinksMode === 'auto' && (
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='url_privacy.redirect_services_url.label' defaultMessage='Redirect services URLs database address' />}
|
||||||
|
hintText={<FormattedMessage id='url_privacy.redirect_services_url.hint' defaultMessage='URLs database in Farside-compatible format, eg. {url}' values={{ url: 'https://raw.githubusercontent.com/benbusby/farside/refs/heads/main/services.json' }} />}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
placeholder={intl.formatMessage(messages.redirectServicesUrlPlaceholder)}
|
||||||
|
value={redirectServicesUrl}
|
||||||
|
onChange={({ target }) => setRedirectServicesUrl(target.value)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{redirectLinksMode === 'manual' && (
|
||||||
|
mappings.map((service) => (
|
||||||
|
<FormGroup
|
||||||
|
key={service.name}
|
||||||
|
labelText={<FormattedMessage id='url_privacy.redirect_services.name' defaultMessage='{name}' values={{ name: service.name }} />}
|
||||||
|
hintText={<FormattedMessage id='url_privacy.redirect_services.patterns' defaultMessage='Matches: {pattern}, eg. {services}, leave empty for no redirect' values={{ pattern: service.urlPattern, services: <FormattedList value={service.targets} /> }} />}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
outerClassName='grow'
|
||||||
|
type='text'
|
||||||
|
value={redirectServices[service.name]}
|
||||||
|
onChange={(e) => setRedirectServices((services) => {
|
||||||
|
services[service.name] = e.target.value;
|
||||||
|
})}
|
||||||
|
placeholder={intl.formatMessage(messages.redirectServicePlaceholder)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
|
||||||
<FormActions>
|
<FormActions>
|
||||||
<Button type='submit'>
|
<Button type='submit'>
|
||||||
<FormattedMessage id='url_privacy.save' defaultMessage='Save' />
|
<FormattedMessage id='url_privacy.save' defaultMessage='Save' />
|
||||||
|
|||||||
@ -1674,6 +1674,17 @@
|
|||||||
"url_privacy.hash_url.hint": "SHA256 hash of rules database, used to avoid unnecessary fetches, eg. {url}",
|
"url_privacy.hash_url.hint": "SHA256 hash of rules database, used to avoid unnecessary fetches, eg. {url}",
|
||||||
"url_privacy.hash_url.label": "URL cleaning rules hash address (optional)",
|
"url_privacy.hash_url.label": "URL cleaning rules hash address (optional)",
|
||||||
"url_privacy.hash_url.placeholder": "Hash URL",
|
"url_privacy.hash_url.placeholder": "Hash URL",
|
||||||
|
"url_privacy.redirect_links_mode": "Redirect links to popular websites to privacy-respecting proxy services",
|
||||||
|
"url_privacy.redirect_links_mode.auto": "From URL",
|
||||||
|
"url_privacy.redirect_links_mode.manual": "Specify manually",
|
||||||
|
"url_privacy.redirect_links_mode.off": "Disabled",
|
||||||
|
"url_privacy.redirect_services.name": "{name}",
|
||||||
|
"url_privacy.redirect_services.patterns": "Matches: {pattern}, eg. {services}, leave empty for no redirect",
|
||||||
|
"url_privacy.redirect_services_update.fail": "Failed to update redirect services URL",
|
||||||
|
"url_privacy.redirect_services_update.success": "Successfully updated redirect services",
|
||||||
|
"url_privacy.redirect_services_url.hint": "URLs database in Farside-compatible format, eg. {url}",
|
||||||
|
"url_privacy.redirect_services_url.label": "Redirect services URLs database address",
|
||||||
|
"url_privacy.redirect_services_url.placeholder": "eg. https://proxy.example.org",
|
||||||
"url_privacy.rules_url.hint": "Rules database in ClearURLs-compatible format, eg. {url}",
|
"url_privacy.rules_url.hint": "Rules database in ClearURLs-compatible format, eg. {url}",
|
||||||
"url_privacy.rules_url.label": "URL cleaning rules database address",
|
"url_privacy.rules_url.label": "URL cleaning rules database address",
|
||||||
"url_privacy.rules_url.placeholder": "Rules URL",
|
"url_privacy.rules_url.placeholder": "Rules URL",
|
||||||
|
|||||||
@ -44,6 +44,9 @@ const settingsSchema = v.object({
|
|||||||
rulesUrl: v.fallback(v.string(), ''),
|
rulesUrl: v.fallback(v.string(), ''),
|
||||||
hashUrl: v.fallback(v.string(), ''),
|
hashUrl: v.fallback(v.string(), ''),
|
||||||
displayTargetHost: v.fallback(v.boolean(), true),
|
displayTargetHost: v.fallback(v.boolean(), true),
|
||||||
|
redirectLinksMode: v.fallback(v.picklist(['off', 'auto', 'manual']), 'off'),
|
||||||
|
redirectServicesUrl: v.fallback(v.string(), ''),
|
||||||
|
redirectServices: v.fallback(v.record(v.string(), v.string()), {}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
theme: v.fallback(v.optional(v.object({
|
theme: v.fallback(v.optional(v.object({
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { create } from 'zustand';
|
|||||||
import { mutative } from 'zustand-mutative';
|
import { mutative } from 'zustand-mutative';
|
||||||
|
|
||||||
import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings';
|
import { settingsSchema, type Settings } from 'pl-fe/schemas/pl-fe/settings';
|
||||||
|
import KVStore from 'pl-fe/storage/kv-store';
|
||||||
import toast from 'pl-fe/toast';
|
import toast from 'pl-fe/toast';
|
||||||
import { updateRulesFromUrl } from 'pl-fe/utils/url-purify';
|
import { KVStoreRedirectServicesItem, resetRules, setManualRedirectServices, updateRedirectServicesFromUrl, updateRulesFromUrl } from 'pl-fe/utils/url-purify';
|
||||||
|
|
||||||
import type { Emoji } from 'pl-fe/features/emoji';
|
import type { Emoji } from 'pl-fe/features/emoji';
|
||||||
import type { store } from 'pl-fe/store';
|
import type { store } from 'pl-fe/store';
|
||||||
@ -15,8 +16,10 @@ let lazyStore: typeof store;
|
|||||||
import('pl-fe/store').then(({ store }) => lazyStore = store).catch(() => {});
|
import('pl-fe/store').then(({ store }) => lazyStore = store).catch(() => {});
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
updateSuccess: { id: 'url_privacy.update.success', defaultMessage: 'Successfully updated rules database' },
|
rulesUpdateSuccess: { id: 'url_privacy.update.success', defaultMessage: 'Successfully updated rules database' },
|
||||||
updateFail: { id: 'url_privacy.update.fail', defaultMessage: 'Failed to update rules database URL' },
|
rulesUpdateFail: { id: 'url_privacy.update.fail', defaultMessage: 'Failed to update rules database URL' },
|
||||||
|
redirectServicesUpdateSuccess: { id: 'url_privacy.redirect_services_update.success', defaultMessage: 'Successfully updated redirect services' },
|
||||||
|
redirectServicesUpdateFail: { id: 'url_privacy.redirect_services_update.fail', defaultMessage: 'Failed to update redirect services URL' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsSchemaPartial = v.partial(settingsSchema);
|
const settingsSchemaPartial = v.partial(settingsSchema);
|
||||||
@ -50,14 +53,35 @@ const changeSetting = (object: APIEntity, path: string[], value: any, root?: Set
|
|||||||
|
|
||||||
const mergeSettings = (state: State, updating = false) => {
|
const mergeSettings = (state: State, updating = false) => {
|
||||||
const mergedSettings = { ...state.defaultSettings, ...state.userSettings };
|
const mergedSettings = { ...state.defaultSettings, ...state.userSettings };
|
||||||
if (updating && mergedSettings.urlPrivacy.rulesUrl && state.settings.urlPrivacy.rulesUrl !== mergedSettings.urlPrivacy.rulesUrl) {
|
if (updating) {
|
||||||
const me = lazyStore?.getState().me;
|
const me = lazyStore?.getState().me;
|
||||||
if (me) {
|
if (me) {
|
||||||
updateRulesFromUrl(me, mergedSettings.urlPrivacy.rulesUrl, mergedSettings.urlPrivacy.hashUrl).then(() => {
|
if (mergedSettings.urlPrivacy.rulesUrl && state.settings.urlPrivacy.rulesUrl !== mergedSettings.urlPrivacy.rulesUrl) {
|
||||||
toast.success(messages.updateSuccess);
|
updateRulesFromUrl(me, mergedSettings.urlPrivacy.rulesUrl, mergedSettings.urlPrivacy.hashUrl).then(() => {
|
||||||
}).catch(() => {
|
toast.success(messages.rulesUpdateSuccess);
|
||||||
toast.error(messages.updateFail);
|
}).catch(() => {
|
||||||
});
|
toast.error(messages.rulesUpdateFail);
|
||||||
|
});
|
||||||
|
} else if (!mergedSettings.urlPrivacy.rulesUrl && state.settings.urlPrivacy.rulesUrl !== mergedSettings.urlPrivacy.rulesUrl) {
|
||||||
|
resetRules(me).then(() => {
|
||||||
|
toast.success(messages.rulesUpdateSuccess);
|
||||||
|
}).catch(() => {
|
||||||
|
toast.error(messages.rulesUpdateFail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (mergedSettings.urlPrivacy.redirectLinksMode === 'auto' && mergedSettings.urlPrivacy.redirectServicesUrl && state.settings.urlPrivacy.redirectServicesUrl !== mergedSettings.urlPrivacy.redirectServicesUrl) {
|
||||||
|
updateRedirectServicesFromUrl(me, mergedSettings.urlPrivacy.redirectServicesUrl).then(() => {
|
||||||
|
toast.success(messages.redirectServicesUpdateSuccess);
|
||||||
|
}).catch(() => {
|
||||||
|
toast.error(messages.redirectServicesUpdateFail);
|
||||||
|
});
|
||||||
|
} else if (mergedSettings.urlPrivacy.redirectLinksMode === 'manual') {
|
||||||
|
setManualRedirectServices(me, mergedSettings.urlPrivacy.redirectServices).then(() => {
|
||||||
|
toast.success(messages.redirectServicesUpdateSuccess);
|
||||||
|
}).catch(() => {
|
||||||
|
toast.error(messages.redirectServicesUpdateFail);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.settings = mergedSettings;
|
state.settings = mergedSettings;
|
||||||
@ -80,6 +104,45 @@ const useSettingsStore = create<State>()(mutative((set) => ({
|
|||||||
if (typeof settings !== 'object') return;
|
if (typeof settings !== 'object') return;
|
||||||
|
|
||||||
state.userSettings = v.parse(settingsSchemaPartial, settings);
|
state.userSettings = v.parse(settingsSchemaPartial, settings);
|
||||||
|
|
||||||
|
const me = lazyStore?.getState().me;
|
||||||
|
if (me) {
|
||||||
|
KVStore.getItem<string>('url-purify-rules:last').then((value) => {
|
||||||
|
if (value !== me) {
|
||||||
|
if (state.userSettings.urlPrivacy?.rulesUrl) {
|
||||||
|
updateRulesFromUrl(me, state.userSettings.urlPrivacy.rulesUrl, state.userSettings.urlPrivacy.hashUrl).then(() => {
|
||||||
|
toast.success(messages.rulesUpdateSuccess);
|
||||||
|
}).catch(() => {
|
||||||
|
toast.error(messages.rulesUpdateFail);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resetRules(me);
|
||||||
|
}
|
||||||
|
switch (state.userSettings.urlPrivacy?.redirectLinksMode) {
|
||||||
|
case 'auto':
|
||||||
|
updateRedirectServicesFromUrl(me, state.userSettings.urlPrivacy?.redirectServicesUrl);
|
||||||
|
break;
|
||||||
|
case 'manual':
|
||||||
|
setManualRedirectServices(me, state.userSettings.urlPrivacy.redirectServices);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setManualRedirectServices(me, {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
KVStore.getItem<KVStoreRedirectServicesItem>(`url-purify-redirect-services:${me}`).then((services) => {
|
||||||
|
if (state.userSettings.urlPrivacy?.redirectLinksMode === 'auto') {
|
||||||
|
if (services?.redirectServicesUrl !== state.userSettings.urlPrivacy?.redirectServicesUrl) {
|
||||||
|
updateRedirectServicesFromUrl(me, state.userSettings.urlPrivacy?.redirectServicesUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setManualRedirectServices(me, state.userSettings.urlPrivacy?.redirectServices || {});
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
mergeSettings(state);
|
mergeSettings(state);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -12,19 +12,25 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
// I hope I got this relicensing stuff right xD
|
// I hope I got this relicensing stuff right xD
|
||||||
import { URLPurify, type SerializedRules } from '@mkljczk/url-purify';
|
import { mappings, URLPurify, type SerializedRules, type SerializedServices } from '@mkljczk/url-purify';
|
||||||
|
|
||||||
import KVStore from 'pl-fe/storage/kv-store';
|
import KVStore from 'pl-fe/storage/kv-store';
|
||||||
import { Me } from 'pl-fe/types/pl-fe';
|
import { Me } from 'pl-fe/types/pl-fe';
|
||||||
|
|
||||||
interface KVStoreItem {
|
interface KVStoreRulesItem {
|
||||||
hashUrl: string;
|
hashUrl?: string;
|
||||||
rulesUrl: string;
|
rulesUrl?: string;
|
||||||
hash: string;
|
hash?: string;
|
||||||
rules: SerializedRules;
|
rules: SerializedRules;
|
||||||
fetchedAt: number;
|
fetchedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KVStoreRedirectServicesItem {
|
||||||
|
redirectServicesUrl?: string;
|
||||||
|
redirectServices: SerializedServices;
|
||||||
|
fetchedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
const sha256 = async (message: string) =>
|
const sha256 = async (message: string) =>
|
||||||
Array.from(new Uint8Array(
|
Array.from(new Uint8Array(
|
||||||
await window.crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(message)),
|
await window.crypto.subtle.digest('SHA-256', (new TextEncoder()).encode(message)),
|
||||||
@ -106,10 +112,17 @@ const DEFAULT_RULESET: SerializedRules = {
|
|||||||
|
|
||||||
const Purify = new URLPurify({
|
const Purify = new URLPurify({
|
||||||
rulesFromMemory: DEFAULT_RULESET,
|
rulesFromMemory: DEFAULT_RULESET,
|
||||||
|
instancePickMode: 'random',
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateRulesFromUrl = async (user: Me, rulesUrl: string, hashUrl: string, oldHash?: string) => {
|
const resetRules = async (user: Me) => {
|
||||||
if (oldHash) {
|
Purify.setRules(DEFAULT_RULESET);
|
||||||
|
await KVStore.setItem('url-purify-rules:last', user);
|
||||||
|
return KVStore.removeItem(`url-purify-rules:${user}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRulesFromUrl = async (user: Me, rulesUrl: string, hashUrl?: string, oldHash?: string) => {
|
||||||
|
if (oldHash && hashUrl) {
|
||||||
const newHash = await fetch(hashUrl).then((response) => response.text());
|
const newHash = await fetch(hashUrl).then((response) => response.text());
|
||||||
if (newHash === oldHash) return;
|
if (newHash === oldHash) return;
|
||||||
}
|
}
|
||||||
@ -121,7 +134,7 @@ const updateRulesFromUrl = async (user: Me, rulesUrl: string, hashUrl: string, o
|
|||||||
Purify.setRules(parsedRules, hash);
|
Purify.setRules(parsedRules, hash);
|
||||||
|
|
||||||
await KVStore.setItem('url-purify-rules:last', user);
|
await KVStore.setItem('url-purify-rules:last', user);
|
||||||
return KVStore.setItem<KVStoreItem>(`url-purify-rules:${user}`, {
|
return KVStore.setItem<KVStoreRulesItem>(`url-purify-rules:${user}`, {
|
||||||
hashUrl,
|
hashUrl,
|
||||||
rulesUrl,
|
rulesUrl,
|
||||||
hash,
|
hash,
|
||||||
@ -130,25 +143,82 @@ const updateRulesFromUrl = async (user: Me, rulesUrl: string, hashUrl: string, o
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateRedirectServicesFromUrl = async (user: Me, redirectServicesUrl: string) => {
|
||||||
|
const redirectServices = await fetch(redirectServicesUrl).then((response) => response.text());
|
||||||
|
|
||||||
|
const parsedRedirectServices = JSON.parse(redirectServices);
|
||||||
|
Purify.setRedirectServices(parsedRedirectServices);
|
||||||
|
|
||||||
|
await KVStore.setItem('url-purify-redirect-services:last', user);
|
||||||
|
return KVStore.setItem<KVStoreRedirectServicesItem>(`url-purify-redirect-services:${user}`, {
|
||||||
|
redirectServicesUrl,
|
||||||
|
redirectServices: parsedRedirectServices,
|
||||||
|
fetchedAt: Date.now(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setManualRedirectServices = async (user: Me, redirectServices: Record<string, string>) => {
|
||||||
|
const parsedRedirectServices: SerializedServices = Object.entries(redirectServices).filter(([_, instance]) => instance.trim()).map((service) => ({
|
||||||
|
fallback: service[1],
|
||||||
|
instances: [service[1]],
|
||||||
|
test_url: '',
|
||||||
|
type: mappings.find((mapping) => mapping.name === service[0])?.targets[0] || 'unknown',
|
||||||
|
}));
|
||||||
|
Purify.setRedirectServices(parsedRedirectServices);
|
||||||
|
|
||||||
|
await KVStore.setItem('url-purify-redirect-services:last', user);
|
||||||
|
return KVStore.setItem<KVStoreRedirectServicesItem>(`url-purify-redirect-services:${user}`, {
|
||||||
|
redirectServices: parsedRedirectServices,
|
||||||
|
fetchedAt: Date.now(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getRulesForUser = (user: Me) => {
|
const getRulesForUser = (user: Me) => {
|
||||||
if (!user || typeof user !== 'string') return;
|
if (!user || typeof user !== 'string') return;
|
||||||
KVStore.getItem<KVStoreItem>(`url-purify-rules:${user}`, (rules) => {
|
KVStore.getItem<KVStoreRulesItem>(`url-purify-rules:${user}`).then((rules) => {
|
||||||
if (!rules) return;
|
if (!rules?.rulesUrl) return;
|
||||||
Purify.setRules(rules.rules, rules.hash);
|
Purify.setRules(rules.rules, rules.hash);
|
||||||
|
|
||||||
if (rules.fetchedAt + 1000 * 60 * 60 * 24 < Date.now()) {
|
if (rules.fetchedAt + 1000 * 60 * 60 * 24 < Date.now()) {
|
||||||
updateRulesFromUrl(user, rules.rulesUrl, rules.hashUrl, rules.hash);
|
updateRulesFromUrl(user, rules.rulesUrl, rules.hashUrl, rules.hash);
|
||||||
}
|
}
|
||||||
});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRulesFromMemory = () => {
|
const getRulesFromMemory = () => {
|
||||||
KVStore.getItem('url-purify-rules:last', (url: string) => {
|
KVStore.getItem<string>('url-purify-rules:last').then((url) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
getRulesForUser(url);
|
getRulesForUser(url);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
getRulesFromMemory();
|
const getRedirectServicesForUser = (user: Me) => {
|
||||||
|
if (!user || typeof user !== 'string') return;
|
||||||
|
KVStore.getItem<KVStoreRedirectServicesItem>(`url-purify-redirect-services:${user}`).then((services) => {
|
||||||
|
if (!services) return;
|
||||||
|
Purify.setRedirectServices(services.redirectServices);
|
||||||
|
|
||||||
export { Purify as default, getRulesForUser, updateRulesFromUrl };
|
if (services.redirectServicesUrl && services.fetchedAt + 1000 * 60 * 60 * 24 < Date.now()) {
|
||||||
|
updateRedirectServicesFromUrl(user, services.redirectServicesUrl);
|
||||||
|
}
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRedirectServicesFromMemory = () => {
|
||||||
|
KVStore.getItem<string>('url-purify-redirect-services:last').then((url) => {
|
||||||
|
if (!url) return;
|
||||||
|
getRedirectServicesForUser(url);
|
||||||
|
}).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
getRulesFromMemory();
|
||||||
|
getRedirectServicesFromMemory();
|
||||||
|
|
||||||
|
export {
|
||||||
|
Purify as default,
|
||||||
|
resetRules,
|
||||||
|
setManualRedirectServices,
|
||||||
|
updateRedirectServicesFromUrl,
|
||||||
|
updateRulesFromUrl,
|
||||||
|
type KVStoreRedirectServicesItem,
|
||||||
|
};
|
||||||
|
|||||||
@ -1775,10 +1775,10 @@
|
|||||||
lodash "^4.17.21"
|
lodash "^4.17.21"
|
||||||
mousetrap "^1.6.5"
|
mousetrap "^1.6.5"
|
||||||
|
|
||||||
"@mkljczk/url-purify@^0.0.2":
|
"@mkljczk/url-purify@^0.0.3":
|
||||||
version "0.0.2"
|
version "0.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@mkljczk/url-purify/-/url-purify-0.0.2.tgz#f6aed0f750ab49b8cf9d3e8169b2b358c1d4519f"
|
resolved "https://registry.yarnpkg.com/@mkljczk/url-purify/-/url-purify-0.0.3.tgz#e5121927617b007d2f91f6e08c73552a4f3c06dc"
|
||||||
integrity sha512-OH5bb84cf7rpwPxE8y0JzkdPMCysC1ZO5wEZZf/cxBGa5io6XKV1f6WHZwU/CYUaBFDYana3ctU4FZdCNH6slg==
|
integrity sha512-3O4QO/nH9yV/GKim+yKvQF1cKWN0dBAsxC5Ve50d1PaUYQFSd4y733eRGt+zRcPZrvgyAkyZZ5Bx7dAHWX+bBQ==
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime@^0.2.7":
|
"@napi-rs/wasm-runtime@^0.2.7":
|
||||||
version "0.2.7"
|
version "0.2.7"
|
||||||
@ -8851,6 +8851,11 @@ uri-js@^4.2.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode "^2.1.0"
|
punycode "^2.1.0"
|
||||||
|
|
||||||
|
use-mutative@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/use-mutative/-/use-mutative-1.2.1.tgz#ee86017899f48027bf87b9d42c6c3a237997a536"
|
||||||
|
integrity sha512-cXvnAWmCxjcxYNitIwLJJEmjLISQTIfj0T88xfk1xNM0knmnOfEQPeU4+CDWBAcK0IXP7ey8fKeGc0seh+5OBA==
|
||||||
|
|
||||||
use-sync-external-store@^1.4.0:
|
use-sync-external-store@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz#adbc795d8eeb47029963016cefdf89dc799fcebc"
|
||||||
|
|||||||
Reference in New Issue
Block a user