diff options
Diffstat (limited to 'web/source/settings/views/moderation')
7 files changed, 1095 insertions, 0 deletions
diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx new file mode 100644 index 000000000..8668caa4b --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx @@ -0,0 +1,181 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useMemo } from "react"; +import { useLocation } from "wouter"; +import { DomainPermSub } from "../../../../lib/types/domain-permission"; +import { yesOrNo } from "../../../../lib/util"; + +export function DomainPermissionSubscriptionHelpText() { + return ( + <> + Domain permission subscriptions allow your instance to "subscribe" to a list of block or allows at a given url. + <br/> + Every 24 hours, each subscribed list is fetched by your instance, and any discovered + permissions in each list are loaded into your instance as blocks/allows/drafts. + </> + ); +} + +export function DomainPermissionSubscriptionDocsLink() { + return ( + <a + href="https://docs.gotosocial.org/en/latest/admin/settings/#domain-permission-subscriptions" + target="_blank" + className="docslink" + rel="noreferrer" + > + Learn more about domain permission subscriptions (opens in a new tab) + </a> + ); +} + +export interface SubscriptionEntryProps { + permSub: DomainPermSub; + linkTo: string; + backLocation: string; +} + +export function SubscriptionListEntry({ permSub, linkTo, backLocation }: SubscriptionEntryProps) { + const [ _location, setLocation ] = useLocation(); + + const permType = permSub.permission_type; + if (!permType) { + throw "permission_type was undefined"; + } + + const { + priority, + title, + uri, + as_draft: asDraft, + adopt_orphans: adoptOrphans, + content_type: contentType, + fetched_at: fetchedAt, + successfully_fetched_at: successfullyFetchedAt, + count, + } = permSub; + + const ariaLabel = useMemo(() => { + let ariaLabel = ""; + + // Prepend title. + if (title.length !== 0) { + ariaLabel += `${title}, create `; + } else { + ariaLabel += "Create "; + } + + // Add perm type. + ariaLabel += permType; + + // Alter wording + // if using drafts. + if (asDraft) { + ariaLabel += " drafts from "; + } else { + ariaLabel += "s from "; + } + + // Add url. + ariaLabel += uri; + + return ariaLabel; + }, [title, permType, asDraft, uri]); + + let fetchedAtStr = "never"; + if (fetchedAt) { + fetchedAtStr = new Date(fetchedAt).toDateString(); + } + + let successfullyFetchedAtStr = "never"; + if (successfullyFetchedAt) { + successfullyFetchedAtStr = new Date(successfullyFetchedAt).toDateString(); + } + + return ( + <span + className={`pseudolink domain-permission-subscription entry`} + aria-label={ariaLabel} + title={ariaLabel} + onClick={() => { + // When clicking on a subscription, direct + // to the detail view for that subscription. + setLocation(linkTo, { + // Store the back location in history so + // the detail view can use it to return to + // this page (including query parameters). + state: { backLocation: backLocation } + }); + }} + role="link" + tabIndex={0} + > + <dl className="info-list"> + { permSub.title !== "" && + <span className="domain-permission-subscription-title"> + {title} + </span> + } + <div className="info-list-entry"> + <dt>Priority:</dt> + <dd>{priority}</dd> + </div> + <div className="info-list-entry"> + <dt>Permission type:</dt> + <dd className={`permission-type ${permType}`}> + <i + aria-hidden={true} + className={`fa fa-${permType === "allow" ? "check" : "close"}`} + ></i> + {permType} + </dd> + </div> + <div className="info-list-entry"> + <dt>URL:</dt> + <dd className="text-cutoff">{uri}</dd> + </div> + <div className="info-list-entry"> + <dt>Content type:</dt> + <dd>{contentType}</dd> + </div> + <div className="info-list-entry"> + <dt>Create as draft:</dt> + <dd>{yesOrNo(asDraft)}</dd> + </div> + <div className="info-list-entry"> + <dt>Adopt orphans:</dt> + <dd>{yesOrNo(adoptOrphans)}</dd> + </div> + <div className="info-list-entry"> + <dt>Last fetch attempt:</dt> + <dd className="text-cutoff">{fetchedAtStr}</dd> + </div> + <div className="info-list-entry"> + <dt>Last successful fetch:</dt> + <dd className="text-cutoff">{successfullyFetchedAtStr}</dd> + </div> + <div className="info-list-entry"> + <dt>Discovered {permType}s:</dt> + <dd>{count}</dd> + </div> + </dl> + </span> + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx new file mode 100644 index 000000000..408d81b92 --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx @@ -0,0 +1,384 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useState } from "react"; +import { useLocation, useParams } from "wouter"; +import { useBaseUrl } from "../../../../lib/navigation/util"; +import BackButton from "../../../../components/back-button"; +import { useGetDomainPermissionSubscriptionQuery, useRemoveDomainPermissionSubscriptionMutation, useUpdateDomainPermissionSubscriptionMutation } from "../../../../lib/query/admin/domain-permissions/subscriptions"; +import { useBoolInput, useNumberInput, useTextInput } from "../../../../lib/form"; +import FormWithData from "../../../../lib/form/form-with-data"; +import { DomainPermSub } from "../../../../lib/types/domain-permission"; +import MutationButton from "../../../../components/form/mutation-button"; +import { Checkbox, NumberInput, Select, TextInput } from "../../../../components/form/inputs"; +import useFormSubmit from "../../../../lib/form/submit"; +import UsernameLozenge from "../../../../components/username-lozenge"; +import { urlValidator } from "../../../../lib/util/formvalidators"; + +export default function DomainPermissionSubscriptionDetail() { + const params = useParams(); + let id = params.permSubId as string | undefined; + if (!id) { + throw "no permSub ID"; + } + + return ( + <FormWithData + dataQuery={useGetDomainPermissionSubscriptionQuery} + queryArg={id} + DataForm={DomainPermSubForm} + /> + ); +} + +function DomainPermSubForm({ data: permSub }: { data: DomainPermSub }) { + const baseUrl = useBaseUrl(); + const backLocation: string = history.state?.backLocation ?? `~${baseUrl}/subscriptions/search`; + + return ( + <div className="domain-permission-subscription-details"> + <h1><BackButton to={backLocation} /> Domain Permission Subscription Detail</h1> + <DomainPermSubDetails permSub={permSub} /> + <UpdateDomainPermSub permSub={permSub} /> + <DeleteDomainPermSub permSub={permSub} backLocation={backLocation} /> + </div> + ); +} + +function DomainPermSubDetails({ permSub }: { permSub: DomainPermSub }) { + const [ location ] = useLocation(); + const baseUrl = useBaseUrl(); + + const permType = permSub.permission_type; + if (!permType) { + throw "permission_type was undefined"; + } + + const created = new Date(permSub.created_at).toDateString(); + let fetchedAtStr = "never"; + if (permSub.fetched_at) { + fetchedAtStr = new Date(permSub.fetched_at).toDateString(); + } + + let successfullyFetchedAtStr = "never"; + if (permSub.successfully_fetched_at) { + successfullyFetchedAtStr = new Date(permSub.successfully_fetched_at).toDateString(); + } + + return ( + <dl className="info-list"> + <div className="info-list-entry"> + <dt>Permission type:</dt> + <dd className={`permission-type ${permType}`}> + <i + aria-hidden={true} + className={`fa fa-${permType === "allow" ? "check" : "close"}`} + ></i> + {permType} + </dd> + </div> + <div className="info-list-entry"> + <dt>ID</dt> + <dd className="monospace">{permSub.id}</dd> + </div> + <div className="info-list-entry"> + <dt>Created</dt> + <dd><time dateTime={permSub.created_at}>{created}</time></dd> + </div> + <div className="info-list-entry"> + <dt>Created By</dt> + <dd> + <UsernameLozenge + account={permSub.created_by} + linkTo={`~/settings/moderation/accounts/${permSub.created_by}`} + backLocation={`~${baseUrl}${location}`} + /> + </dd> + </div> + <div className="info-list-entry"> + <dt>Last fetch attempt:</dt> + <dd>{fetchedAtStr}</dd> + </div> + <div className="info-list-entry"> + <dt>Last successful fetch:</dt> + <dd>{successfullyFetchedAtStr}</dd> + </div> + <div className="info-list-entry"> + <dt>Discovered {permSub.permission_type}s:</dt> + <dd>{permSub.count}</dd> + </div> + </dl> + ); +} + +function UpdateDomainPermSub({ permSub }: { permSub: DomainPermSub }) { + const [ showPassword, setShowPassword ] = useState(false); + const form = { + priority: useNumberInput("priority", { source: permSub }), + uri: useTextInput("uri", { + source: permSub, + validator: urlValidator, + }), + content_type: useTextInput("content_type", { source: permSub }), + title: useTextInput("title", { source: permSub }), + as_draft: useBoolInput("as_draft", { source: permSub }), + adopt_orphans: useBoolInput("adopt_orphans", { source: permSub }), + useBasicAuth: useBoolInput("useBasicAuth", { + defaultValue: + (permSub.fetch_password !== undefined && permSub.fetch_password !== "") || + (permSub.fetch_username !== undefined && permSub.fetch_username !== ""), + nosubmit: true + }), + fetch_username: useTextInput("fetch_username", { + source: permSub + }), + fetch_password: useTextInput("fetch_password", { + source: permSub + }), + }; + + const [submitUpdate, updateResult] = useFormSubmit( + form, + useUpdateDomainPermissionSubscriptionMutation(), + { + changedOnly: true, + customizeMutationArgs: (mutationData) => { + // Clear username + password if they were set, + // but user has selected to not use basic auth. + if (!form.useBasicAuth.value) { + if (permSub.fetch_username !== undefined && permSub.fetch_username !== "") { + mutationData["fetch_username"] = ""; + } + if (permSub.fetch_password !== undefined && permSub.fetch_password !== "") { + mutationData["fetch_password"] = ""; + } + } + + // Remove useBasicAuth if included. + delete mutationData["useBasicAuth"]; + + // Modify mutation argument to + // include ID and permission type. + return { + id: permSub.id, + permType: permSub.permission_type, + formData: mutationData, + }; + }, + onFinish: res => { + // On a successful response that returns data, + // clear the fetch_username and fetch_password + // fields if they weren't set on the returned sub. + if (res.data) { + if (res.data.fetch_username === undefined || res.data.fetch_username === "") { + form.fetch_username.setter(""); + } + if (res.data.fetch_password === undefined || res.data.fetch_password === "") { + form.fetch_password.setter(""); + } + } + } + } + ); + + const submitDisabled = () => { + // If no basic auth, we don't care what + // fetch_password and fetch_username are. + if (!form.useBasicAuth.value) { + return false; + } + + // Either of fetch_password or fetch_username must be set. + return !(form.fetch_password.value || form.fetch_username.value); + }; + + return ( + <form + className="domain-permission-subscription-update" + onSubmit={submitUpdate} + // Prevent password managers + // trying to fill in fields. + autoComplete="off" + > + <h2>Edit Subscription</h2> + <TextInput + field={form.title} + label={`Subscription title`} + placeholder={`Some List of ${permSub.permission_type === "block" ? "Baddies" : "Goodies"}`} + autoCapitalize="words" + spellCheck="false" + /> + + <NumberInput + field={form.priority} + label={`Subscription priority (0-255)`} + type="number" + min="0" + max="255" + /> + + <TextInput + field={form.uri} + label={`Permission list URL (http or https)`} + placeholder="https://example.org/files/some_list_somewhere" + autoCapitalize="none" + spellCheck="false" + type="url" + /> + + <Select + field={form.content_type} + label="Content type" + options={ + <> + <option value="text/csv">CSV</option> + <option value="application/json">JSON</option> + <option value="text/plain">Plain</option> + </> + } + /> + + <Checkbox + label={ + <> + <>Use </> + <a + href="https://en.wikipedia.org/wiki/Basic_access_authentication" + target="_blank" + rel="noreferrer" + >basic auth</a> + <> when fetching</> + </> + } + field={form.useBasicAuth} + /> + + { form.useBasicAuth.value && + <> + <TextInput + field={form.fetch_username} + label={`Basic auth username`} + autoCapitalize="none" + spellCheck="false" + autoComplete="off" + required={form.useBasicAuth.value && !form.fetch_password.value} + /> + <div className="password-show-hide"> + <TextInput + field={form.fetch_password} + label={`Basic auth password`} + autoCapitalize="none" + spellCheck="false" + type={showPassword ? "" : "password"} + autoComplete="off" + required={form.useBasicAuth.value && !form.fetch_username.value} + /> + <button + className="password-show-hide-toggle" + type="button" + title={!showPassword ? "Show password" : "Hide password"} + onClick={e => { + e.preventDefault(); + setShowPassword(!showPassword); + }} + > + { !showPassword ? "Show" : "Hide" } + </button> + </div> + </> + } + + <Checkbox + label="Adopt orphan permissions" + field={form.adopt_orphans} + /> + + <Checkbox + label="Create permissions as drafts" + field={form.as_draft} + /> + + { !form.as_draft.value && + <div className="info"> + <i className="fa fa-fw fa-exclamation-circle" aria-hidden="true"></i> + <b> + Unchecking "create permissions as drafts" means that permissions found on the + subscribed list will be enforced immediately the next time the list is fetched. + <br/> + If you're subscribing to a block list, this means that blocks will be created + automatically from the given list, potentially severing any existing follow + relationships with accounts on the blocked domain. + <br/> + Before saving, make sure this is what you really want to do, and consider + creating domain excludes for domains that you want to manage manually. + </b> + </div> + } + + <MutationButton + label="Save" + result={updateResult} + disabled={submitDisabled()} + /> + + </form> + ); +} + +function DeleteDomainPermSub({ permSub, backLocation }: { permSub: DomainPermSub, backLocation: string }) { + const permType = permSub.permission_type; + if (!permType) { + throw "permission_type was undefined"; + } + + const [_location, setLocation] = useLocation(); + const [ removeSub, result ] = useRemoveDomainPermissionSubscriptionMutation(); + const removeChildren = useBoolInput("remove_children", { defaultValue: false }); + + return ( + <form className="domain-permission-subscription-remove"> + <h2>Remove Subscription</h2> + + <Checkbox + label={`Also remove any ${permType}s created by this subscription`} + field={removeChildren} + /> + + <MutationButton + label={`Remove`} + title={`Remove`} + type="button" + className="button danger" + onClick={(e) => { + e.preventDefault(); + const id = permSub.id; + const remove_children = removeChildren.value as boolean; + removeSub({ id, remove_children }).then(res => { + if ("data" in res) { + setLocation(backLocation); + } + }); + }} + disabled={false} + showError={true} + result={result} + /> + </form> + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx new file mode 100644 index 000000000..10f6fd1cf --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx @@ -0,0 +1,170 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { ReactNode, useEffect, useMemo } from "react"; + +import { useTextInput } from "../../../../lib/form"; +import { PageableList } from "../../../../components/pageable-list"; +import MutationButton from "../../../../components/form/mutation-button"; +import { useLocation, useSearch } from "wouter"; +import { useLazySearchDomainPermissionSubscriptionsQuery } from "../../../../lib/query/admin/domain-permissions/subscriptions"; +import { DomainPermSub } from "../../../../lib/types/domain-permission"; +import { Select } from "../../../../components/form/inputs"; +import { DomainPermissionSubscriptionDocsLink, DomainPermissionSubscriptionHelpText, SubscriptionListEntry } from "./common"; + +export default function DomainPermissionSubscriptionsSearch() { + return ( + <div className="domain-permission-subscriptions-view"> + <div className="form-section-docs"> + <h1>Domain Permission Subscriptions</h1> + <p> + You can use the form below to search through domain permission + subscriptions, sorted by creation time (newer to older). + <br/> + <DomainPermissionSubscriptionHelpText /> + </p> + <DomainPermissionSubscriptionDocsLink /> + </div> + <DomainPermissionSubscriptionsSearchForm /> + </div> + ); +} + +function DomainPermissionSubscriptionsSearchForm() { + const [ location, setLocation ] = useLocation(); + const search = useSearch(); + const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]); + const hasParams = urlQueryParams.size != 0; + const [ searchSubscriptions, searchRes ] = useLazySearchDomainPermissionSubscriptionsQuery(); + + const form = { + permission_type: useTextInput("permission_type", { defaultValue: urlQueryParams.get("permission_type") ?? "" }), + limit: useTextInput("limit", { defaultValue: urlQueryParams.get("limit") ?? "20" }) + }; + + // On mount, if urlQueryParams were provided, + // trigger the search. For example, if page + // was accessed at /search?origin=local&limit=20, + // then run a search with origin=local and + // limit=20 and immediately render the results. + // + // If no urlQueryParams set, trigger default + // search (first page, no filtering). + useEffect(() => { + if (hasParams) { + searchSubscriptions(Object.fromEntries(urlQueryParams)); + } else { + setLocation(location + "?limit=20"); + } + }, [ + urlQueryParams, + hasParams, + searchSubscriptions, + location, + setLocation, + ]); + + // Rather than triggering the search directly, + // the "submit" button changes the location + // based on form field params, and lets the + // useEffect hook above actually do the search. + function submitQuery(e) { + e.preventDefault(); + + // Parse query parameters. + const entries = Object.entries(form).map(([k, v]) => { + // Take only defined form fields. + if (v.value === undefined || v.value.length === 0 || v.value === "any") { + return null; + } + return [[k, v.value]]; + }).flatMap(kv => { + // Remove any nulls. + return kv || []; + }); + + const searchParams = new URLSearchParams(entries); + setLocation(location + "?" + searchParams.toString()); + } + + // Location to return to when user clicks "back" on the detail view. + const backLocation = location + (hasParams ? `?${urlQueryParams}` : ""); + + // Function to map an item to a list entry. + function itemToEntry(permSub: DomainPermSub): ReactNode { + return ( + <SubscriptionListEntry + key={permSub.id} + permSub={permSub} + linkTo={`/subscriptions/${permSub.id}`} + backLocation={backLocation} + /> + ); + } + + return ( + <> + <form + onSubmit={submitQuery} + // Prevent password managers + // trying to fill in fields. + autoComplete="off" + > + <Select + field={form.permission_type} + label="Permission type" + options={ + <> + <option value="">Any</option> + <option value="block">Block</option> + <option value="allow">Allow</option> + </> + } + ></Select> + <Select + field={form.limit} + label="Items per page" + options={ + <> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </> + } + ></Select> + <MutationButton + disabled={false} + label={"Search"} + result={searchRes} + /> + </form> + <PageableList + isLoading={searchRes.isLoading} + isFetching={searchRes.isFetching} + isSuccess={searchRes.isSuccess} + items={searchRes.data?.subs} + itemToEntry={itemToEntry} + isError={searchRes.isError} + error={searchRes.error} + emptyMessage={<b>No subscriptions found that match your query.</b>} + prevNextLinks={searchRes.data?.links} + /> + </> + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx new file mode 100644 index 000000000..e29e3d755 --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx @@ -0,0 +1,230 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useState } from "react"; +import useFormSubmit from "../../../../lib/form/submit"; +import { useCreateDomainPermissionSubscriptionMutation } from "../../../../lib/query/admin/domain-permissions/subscriptions"; +import { useBoolInput, useNumberInput, useTextInput } from "../../../../lib/form"; +import { urlValidator } from "../../../../lib/util/formvalidators"; +import MutationButton from "../../../../components/form/mutation-button"; +import { Checkbox, NumberInput, Select, TextInput } from "../../../../components/form/inputs"; +import { useLocation } from "wouter"; +import { DomainPermissionSubscriptionDocsLink, DomainPermissionSubscriptionHelpText } from "./common"; + +export default function DomainPermissionSubscriptionNew() { + const [ _location, setLocation ] = useLocation(); + + const useBasicAuth = useBoolInput("useBasicAuth", { defaultValue: false }); + const form = { + priority: useNumberInput("priority", { defaultValue: 0 }), + uri: useTextInput("uri", { + validator: urlValidator, + }), + content_type: useTextInput("content_type", { defaultValue: "text/csv" }), + permission_type: useTextInput("permission_type", { defaultValue: "block" }), + title: useTextInput("title"), + as_draft: useBoolInput("as_draft", { defaultValue: true }), + adopt_orphans: useBoolInput("adopt_orphans", { defaultValue: false }), + fetch_username: useTextInput("fetch_username", { + nosubmit: !useBasicAuth.value + }), + fetch_password: useTextInput("fetch_password", { + nosubmit: !useBasicAuth.value + }), + }; + + const [ showPassword, setShowPassword ] = useState(false); + + const [formSubmit, result] = useFormSubmit( + form, + useCreateDomainPermissionSubscriptionMutation(), + { + changedOnly: false, + onFinish: (res) => { + if (res.data) { + // Creation successful, + // redirect to subscription detail. + setLocation(`/subscriptions/${res.data.id}`); + } + }, + }); + + const submitDisabled = () => { + // URI required. + if (!form.uri.value || !form.uri.valid) { + return true; + } + + // If no basic auth, we don't care what + // fetch_password and fetch_username are. + if (!useBasicAuth.value) { + return false; + } + + // Either of fetch_password or fetch_username must be set. + return !(form.fetch_password.value || form.fetch_username.value); + }; + + return ( + <form + className="domain-permission-subscription-create" + onSubmit={formSubmit} + // Prevent password managers + // trying to fill in fields. + autoComplete="off" + > + <div className="form-section-docs"> + <h2>New Domain Permission Subscription</h2> + <p><DomainPermissionSubscriptionHelpText /></p> + <DomainPermissionSubscriptionDocsLink /> + </div> + + <TextInput + field={form.title} + label={`Subscription title`} + placeholder={`Some List of ${form.permission_type.value === "block" ? "Baddies" : "Goodies"}`} + autoCapitalize="words" + spellCheck="false" + /> + + <NumberInput + field={form.priority} + label={`Subscription priority (0-255)`} + type="number" + min="0" + max="255" + /> + + <Select + field={form.permission_type} + label="Permission type" + options={ + <> + <option value="block">Block</option> + <option value="allow">Allow</option> + </> + } + /> + + <TextInput + field={form.uri} + label={`Permission list URL (http or https)`} + placeholder="https://example.org/files/some_list_somewhere" + autoCapitalize="none" + spellCheck="false" + type="url" + /> + + <Select + field={form.content_type} + label="Content type" + options={ + <> + <option value="text/csv">CSV</option> + <option value="application/json">JSON</option> + <option value="text/plain">Plain</option> + </> + } + /> + + <Checkbox + label={ + <> + <>Use </> + <a + href="https://en.wikipedia.org/wiki/Basic_access_authentication" + target="_blank" + rel="noreferrer" + >basic auth</a> + <> when fetching</> + </> + } + field={useBasicAuth} + /> + + { useBasicAuth.value && + <> + <TextInput + field={form.fetch_username} + label={`Basic auth username`} + autoCapitalize="none" + spellCheck="false" + autoComplete="off" + required={useBasicAuth.value && !form.fetch_password.value} + /> + <div className="password-show-hide"> + <TextInput + field={form.fetch_password} + label={`Basic auth password`} + autoCapitalize="none" + spellCheck="false" + type={showPassword ? "" : "password"} + autoComplete="off" + required={useBasicAuth.value && !form.fetch_username.value} + /> + <button + className="password-show-hide-toggle" + type="button" + title={!showPassword ? "Show password" : "Hide password"} + onClick={e => { + e.preventDefault(); + setShowPassword(!showPassword); + }} + > + { !showPassword ? "Show" : "Hide" } + </button> + </div> + </> + } + + <Checkbox + label="Adopt orphan permissions" + field={form.adopt_orphans} + /> + + <Checkbox + label="Create permissions as drafts" + field={form.as_draft} + /> + + { !form.as_draft.value && + <div className="info"> + <i className="fa fa-fw fa-exclamation-circle" aria-hidden="true"></i> + <b> + Unchecking "create permissions as drafts" means that permissions found on the + subscribed list will be enforced immediately the next time the list is fetched. + <br/> + If you're subscribing to a block list, this means that blocks will be created + automatically from the given list, potentially severing any existing follow + relationships with accounts on the blocked domain. + <br/> + Before saving, make sure this is what you really want to do, and consider + creating domain excludes for domains that you want to manage manually. + </b> + </div> + } + + <MutationButton + label="Save" + result={result} + disabled={submitDisabled()} + /> + </form> + ); +} diff --git a/web/source/settings/views/moderation/domain-permissions/subscriptions/preview.tsx b/web/source/settings/views/moderation/domain-permissions/subscriptions/preview.tsx new file mode 100644 index 000000000..a23c18c9e --- /dev/null +++ b/web/source/settings/views/moderation/domain-permissions/subscriptions/preview.tsx @@ -0,0 +1,100 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { ReactNode } from "react"; + +import { useTextInput } from "../../../../lib/form"; +import { PageableList } from "../../../../components/pageable-list"; +import { useLocation } from "wouter"; +import { useGetDomainPermissionSubscriptionsPreviewQuery } from "../../../../lib/query/admin/domain-permissions/subscriptions"; +import { DomainPermSub } from "../../../../lib/types/domain-permission"; +import { Select } from "../../../../components/form/inputs"; +import { DomainPermissionSubscriptionDocsLink, SubscriptionListEntry } from "./common"; +import { PermType } from "../../../../lib/types/perm"; + +export default function DomainPermissionSubscriptionsPreview() { + return ( + <div className="domain-permission-subscriptions-preview"> + <div className="form-section-docs"> + <h1>Domain Permission Subscriptions Preview</h1> + <p> + You can use the form below to view through domain permission subscriptions sorted by priority (high to low). + <br/> + This reflects the order in which they will actually be fetched by your instance, with higher-priority subscriptions + creating permissions first, followed by lower-priority subscriptions. + </p> + <DomainPermissionSubscriptionDocsLink /> + </div> + <DomainPermissionSubscriptionsPreviewForm /> + </div> + ); +} + +function DomainPermissionSubscriptionsPreviewForm() { + const [ location, _setLocation ] = useLocation(); + + const permType = useTextInput("permission_type", { defaultValue: "block" }); + const { + data: permSubs, + isLoading, + isFetching, + isSuccess, + isError, + error, + } = useGetDomainPermissionSubscriptionsPreviewQuery(permType.value as PermType); + + // Function to map an item to a list entry. + function itemToEntry(permSub: DomainPermSub): ReactNode { + return ( + <SubscriptionListEntry + key={permSub.id} + permSub={permSub} + linkTo={`/subscriptions/${permSub.id}`} + backLocation={location} + /> + ); + } + + return ( + <> + <form> + <Select + field={permType} + label="Permission type" + options={ + <> + <option value="block">Block</option> + <option value="allow">Allow</option> + </> + } + ></Select> + </form> + <PageableList + isLoading={isLoading} + isFetching={isFetching} + isSuccess={isSuccess} + items={permSubs} + itemToEntry={itemToEntry} + isError={isError} + error={error} + emptyMessage={<b>No {permType.value}list subscriptions found.</b>} + /> + </> + ); +} diff --git a/web/source/settings/views/moderation/menu.tsx b/web/source/settings/views/moderation/menu.tsx index 7ac6f9327..17b2f18e0 100644 --- a/web/source/settings/views/moderation/menu.tsx +++ b/web/source/settings/views/moderation/menu.tsx @@ -150,6 +150,28 @@ function ModerationDomainPermsMenu() { icon="fa-plus" /> </MenuItem> + <MenuItem + name="Subscriptions" + itemUrl="subscriptions" + defaultChild="search" + icon="fa-cloud-download" + > + <MenuItem + name="Search" + itemUrl="search" + icon="fa-list" + /> + <MenuItem + name="New subscription" + itemUrl="new" + icon="fa-plus" + /> + <MenuItem + name="Preview" + itemUrl="preview" + icon="fa-eye" + /> + </MenuItem> </MenuItem> ); } diff --git a/web/source/settings/views/moderation/router.tsx b/web/source/settings/views/moderation/router.tsx index 779498ffe..90214188f 100644 --- a/web/source/settings/views/moderation/router.tsx +++ b/web/source/settings/views/moderation/router.tsx @@ -35,6 +35,10 @@ import DomainPermissionDraftDetail from "./domain-permissions/drafts/detail"; import DomainPermissionExcludeDetail from "./domain-permissions/excludes/detail"; import DomainPermissionExcludesSearch from "./domain-permissions/excludes"; import DomainPermissionExcludeNew from "./domain-permissions/excludes/new"; +import DomainPermissionSubscriptionsSearch from "./domain-permissions/subscriptions"; +import DomainPermissionSubscriptionNew from "./domain-permissions/subscriptions/new"; +import DomainPermissionSubscriptionDetail from "./domain-permissions/subscriptions/detail"; +import DomainPermissionSubscriptionsPreview from "./domain-permissions/subscriptions/preview"; /* EXPORTED COMPONENTS @@ -151,6 +155,10 @@ function ModerationDomainPermsRouter() { <Route path="/excludes/search" component={DomainPermissionExcludesSearch} /> <Route path="/excludes/new" component={DomainPermissionExcludeNew} /> <Route path="/excludes/:excludeId" component={DomainPermissionExcludeDetail} /> + <Route path="/subscriptions/search" component={DomainPermissionSubscriptionsSearch} /> + <Route path="/subscriptions/new" component={DomainPermissionSubscriptionNew} /> + <Route path="/subscriptions/preview" component={DomainPermissionSubscriptionsPreview} /> + <Route path="/subscriptions/:permSubId" component={DomainPermissionSubscriptionDetail} /> <Route path="/:permType" component={DomainPermissionsOverview} /> <Route path="/:permType/:domain" component={DomainPermDetail} /> <Route><Redirect to="/blocks"/></Route> |