From e9bb7ddd3aa11da5c48a75c4a600f8fe5cc1c990 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 5 Jan 2025 13:20:33 +0100 Subject: [feature] Create/update/remove domain permission subscriptions (#3623) * [feature] Create/update/remove domain permission subscriptions * lint * envparsing * remove errant fmt.Println * create drafts, subs, exclude, from snapshot models * name etag column correctly * remove count column * lint --- web/source/settings/components/form/inputs.tsx | 27 ++ web/source/settings/lib/form/index.ts | 3 +- web/source/settings/lib/form/number.tsx | 104 ++++++ web/source/settings/lib/form/submit.ts | 23 +- web/source/settings/lib/form/types.ts | 7 + .../admin/domain-permissions/subscriptions.ts | 164 +++++++++ web/source/settings/lib/query/gts-api.ts | 3 +- web/source/settings/lib/types/domain-permission.ts | 154 +++++++++ .../settings/lib/types/permsubcontenttype.ts | 20 ++ web/source/settings/lib/util/formvalidators.ts | 19 + web/source/settings/style.css | 44 ++- .../domain-permissions/subscriptions/common.tsx | 181 ++++++++++ .../domain-permissions/subscriptions/detail.tsx | 384 +++++++++++++++++++++ .../domain-permissions/subscriptions/index.tsx | 170 +++++++++ .../domain-permissions/subscriptions/new.tsx | 230 ++++++++++++ .../domain-permissions/subscriptions/preview.tsx | 100 ++++++ web/source/settings/views/moderation/menu.tsx | 22 ++ web/source/settings/views/moderation/router.tsx | 8 + 18 files changed, 1656 insertions(+), 7 deletions(-) create mode 100644 web/source/settings/lib/form/number.tsx create mode 100644 web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts create mode 100644 web/source/settings/lib/types/permsubcontenttype.ts create mode 100644 web/source/settings/views/moderation/domain-permissions/subscriptions/common.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/subscriptions/detail.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/subscriptions/index.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/subscriptions/new.tsx create mode 100644 web/source/settings/views/moderation/domain-permissions/subscriptions/preview.tsx (limited to 'web/source') diff --git a/web/source/settings/components/form/inputs.tsx b/web/source/settings/components/form/inputs.tsx index 06075ea87..498499db6 100644 --- a/web/source/settings/components/form/inputs.tsx +++ b/web/source/settings/components/form/inputs.tsx @@ -26,6 +26,7 @@ import type { import type { FileFormInputHook, + NumberFormInputHook, RadioFormInputHook, TextFormInputHook, } from "../../lib/form/types"; @@ -57,6 +58,32 @@ export function TextInput({label, field, ...props}: TextInputProps) { ); } +export interface NumberInputProps extends React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement +> { + label?: ReactNode; + field: NumberFormInputHook; +} + +export function NumberInput({label, field, ...props}: NumberInputProps) { + const { onChange, value, ref } = field; + + return ( +
+ +
+ ); +} + export interface TextAreaProps extends React.DetailedHTMLProps< React.TextareaHTMLAttributes, HTMLTextAreaElement diff --git a/web/source/settings/lib/form/index.ts b/web/source/settings/lib/form/index.ts index 409ef0328..878b7c79b 100644 --- a/web/source/settings/lib/form/index.ts +++ b/web/source/settings/lib/form/index.ts @@ -41,6 +41,7 @@ import type { ChecklistInputHook, FieldArrayInputHook, ArrayInputHook, + NumberFormInputHook, } from "./types"; function capitalizeFirst(str: string) { @@ -102,11 +103,11 @@ function value(name: string, initialValue: T) { name, Name: "", value: initialValue, - hasChanged: () => true, // always included }; } export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts) => TextFormInputHook; +export const useNumberInput = inputHook(text) as (_name: string, _opts?: HookOpts) => NumberFormInputHook; export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts) => FileFormInputHook; export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts) => BoolFormInputHook; export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts) => RadioFormInputHook; diff --git a/web/source/settings/lib/form/number.tsx b/web/source/settings/lib/form/number.tsx new file mode 100644 index 000000000..15acf162a --- /dev/null +++ b/web/source/settings/lib/form/number.tsx @@ -0,0 +1,104 @@ +/* + 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 . +*/ + +import React, { + useState, + useRef, + useTransition, + useEffect, +} from "react"; + +import type { + CreateHookNames, + HookOpts, + NumberFormInputHook, +} from "./types"; + +const _default = 0; + +export default function useNumberInput( + { name, Name }: CreateHookNames, + { + initialValue = _default, + dontReset = false, + validator, + showValidation = true, + initValidation, + nosubmit = false, + }: HookOpts +): NumberFormInputHook { + const [number, setNumber] = useState(initialValue); + const numberRef = useRef(null); + + const [validation, setValidation] = useState(initValidation ?? ""); + const [_isValidating, startValidation] = useTransition(); + const valid = validation == ""; + + function onChange(e: React.ChangeEvent) { + const input = e.target.valueAsNumber; + setNumber(input); + + if (validator) { + startValidation(() => { + setValidation(validator(input)); + }); + } + } + + function reset() { + if (!dontReset) { + setNumber(initialValue); + } + } + + useEffect(() => { + if (validator && numberRef.current) { + if (showValidation) { + numberRef.current.setCustomValidity(validation); + } else { + numberRef.current.setCustomValidity(""); + } + } + }, [validation, validator, showValidation]); + + // Array / Object hybrid, for easier access in different contexts + return Object.assign([ + onChange, + reset, + { + [name]: number, + [`${name}Ref`]: numberRef, + [`set${Name}`]: setNumber, + [`${name}Valid`]: valid, + } + ], { + onChange, + reset, + name, + Name: "", // Will be set by inputHook function. + nosubmit, + value: number, + ref: numberRef, + setter: setNumber, + valid, + validate: () => setValidation(validator ? validator(number): ""), + hasChanged: () => number != initialValue, + _default + }); +} diff --git a/web/source/settings/lib/form/submit.ts b/web/source/settings/lib/form/submit.ts index aa662086f..498481deb 100644 --- a/web/source/settings/lib/form/submit.ts +++ b/web/source/settings/lib/form/submit.ts @@ -34,8 +34,24 @@ import type { } from "./types"; interface UseFormSubmitOptions { + /** + * Include only changed fields when submitting the form. + * If no fields have been changed, submit will be a noop. + */ changedOnly: boolean; + /** + * Optional function to run when the form has been sent + * and a response has been returned from the server. + */ onFinish?: ((_res: any) => void); + /** + * Can be optionally used to modify the final mutation argument from the + * gathered mutation data before it's passed into the trigger function. + * + * Useful if the mutation trigger function takes not just a simple key/value + * object but a more complicated object. + */ + customizeMutationArgs?: (_mutationData: { [k: string]: any }) => any; } /** @@ -105,7 +121,7 @@ export default function useFormSubmit( usedAction.current = action; // Transform the hooked form into an object. - const { + let { mutationData, updatedFields, } = getFormMutations(form, { changedOnly }); @@ -117,7 +133,12 @@ export default function useFormSubmit( return; } + // Final tweaks on the mutation + // argument before triggering it. mutationData.action = action; + if (opts.customizeMutationArgs) { + mutationData = opts.customizeMutationArgs(mutationData); + } try { const res = await runMutation(mutationData); diff --git a/web/source/settings/lib/form/types.ts b/web/source/settings/lib/form/types.ts index 17fbec53a..6a4162f3a 100644 --- a/web/source/settings/lib/form/types.ts +++ b/web/source/settings/lib/form/types.ts @@ -181,6 +181,13 @@ export interface TextFormInputHook extends FormInputHook, _withValidate, _withRef {} +export interface NumberFormInputHook extends FormInputHook, + _withSetter, + _withOnChange, + _withReset, + _withValidate, + _withRef {} + export interface RadioFormInputHook extends FormInputHook, _withSetter, _withOnChange, diff --git a/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts new file mode 100644 index 000000000..f065aaf54 --- /dev/null +++ b/web/source/settings/lib/query/admin/domain-permissions/subscriptions.ts @@ -0,0 +1,164 @@ +/* + 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 . +*/ + +import { gtsApi } from "../../gts-api"; + +import type { + DomainPermSub, + DomainPermSubCreateUpdateParams, + DomainPermSubSearchParams, + DomainPermSubSearchResp, +} from "../../../types/domain-permission"; +import parse from "parse-link-header"; +import { PermType } from "../../../types/perm"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + searchDomainPermissionSubscriptions: build.query({ + query: (form) => { + const params = new(URLSearchParams); + Object.entries(form).forEach(([k, v]) => { + if (v !== undefined) { + params.append(k, v); + } + }); + + let query = ""; + if (params.size !== 0) { + query = `?${params.toString()}`; + } + + return { + url: `/api/v1/admin/domain_permission_subscriptions${query}` + }; + }, + // Headers required for paging. + transformResponse: (apiResp: DomainPermSub[], meta) => { + const subs = apiResp; + const linksStr = meta?.response?.headers.get("Link"); + const links = parse(linksStr); + return { subs, links }; + }, + // Only provide TRANSFORMED tag id since this model is not the same + // as getDomainPermissionSubscription model (due to transformResponse). + providesTags: [{ type: "DomainPermissionSubscription", id: "TRANSFORMED" }] + }), + + getDomainPermissionSubscriptionsPreview: build.query({ + query: (permType) => ({ + url: `/api/v1/admin/domain_permission_subscriptions/preview?permission_type=${permType}` + }), + providesTags: (_result, _error, permType) => + // Cache by permission type. + [{ type: "DomainPermissionSubscription", id: `${permType}sByPriority` }] + }), + + getDomainPermissionSubscription: build.query({ + query: (id) => ({ + url: `/api/v1/admin/domain_permission_subscriptions/${id}` + }), + providesTags: (_result, _error, id) => [ + { type: 'DomainPermissionSubscription', id } + ], + }), + + createDomainPermissionSubscription: build.mutation({ + query: (formData) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_subscriptions`, + asForm: true, + body: formData, + discardEmpty: true + }), + invalidatesTags: (_res, _error, formData) => + [ + // Invalidate transformed list of all perm subs. + { type: "DomainPermissionSubscription", id: "TRANSFORMED" }, + // Invalidate perm subs of this type sorted by priority. + { type: "DomainPermissionSubscription", id: `${formData.permission_type}sByPriority` } + ] + }), + + updateDomainPermissionSubscription: build.mutation({ + query: ({ id, formData }) => ({ + method: "PATCH", + url: `/api/v1/admin/domain_permission_subscriptions/${id}`, + asForm: true, + body: formData, + }), + invalidatesTags: (_res, _error, { id, permType }) => + [ + // Invalidate this perm sub. + { type: "DomainPermissionSubscription", id: id }, + // Invalidate transformed list of all perms subs. + { type: "DomainPermissionSubscription", id: "TRANSFORMED" }, + // Invalidate perm subs of this type sorted by priority. + { type: "DomainPermissionSubscription", id: `${permType}sByPriority` } + ], + }), + + removeDomainPermissionSubscription: build.mutation({ + query: ({ id, remove_children }) => ({ + method: "POST", + url: `/api/v1/admin/domain_permission_subscriptions/${id}/remove`, + asForm: true, + body: { remove_children: remove_children }, + }), + }) + }), +}); + +/** + * View domain permission subscriptions. + */ +const useLazySearchDomainPermissionSubscriptionsQuery = extended.useLazySearchDomainPermissionSubscriptionsQuery; + +/** + * Get domain permission subscription with the given ID. + */ +const useGetDomainPermissionSubscriptionQuery = extended.useGetDomainPermissionSubscriptionQuery; + +/** + * Create a domain permission subscription with the given parameters. + */ +const useCreateDomainPermissionSubscriptionMutation = extended.useCreateDomainPermissionSubscriptionMutation; + +/** + * View domain permission subscriptions of selected perm type, sorted by priority descending. + */ +const useGetDomainPermissionSubscriptionsPreviewQuery = extended.useGetDomainPermissionSubscriptionsPreviewQuery; + +/** + * Update domain permission subscription. + */ +const useUpdateDomainPermissionSubscriptionMutation = extended.useUpdateDomainPermissionSubscriptionMutation; + +/** + * Remove a domain permission subscription and optionally its children (harsh). + */ +const useRemoveDomainPermissionSubscriptionMutation = extended.useRemoveDomainPermissionSubscriptionMutation; + +export { + useLazySearchDomainPermissionSubscriptionsQuery, + useGetDomainPermissionSubscriptionQuery, + useCreateDomainPermissionSubscriptionMutation, + useGetDomainPermissionSubscriptionsPreviewQuery, + useUpdateDomainPermissionSubscriptionMutation, + useRemoveDomainPermissionSubscriptionMutation, +}; diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index 9543819a9..34b66913a 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -170,7 +170,8 @@ export const gtsApi = createApi({ "DefaultInteractionPolicies", "InteractionRequest", "DomainPermissionDraft", - "DomainPermissionExclude" + "DomainPermissionExclude", + "DomainPermissionSubscription" ], endpoints: (build) => ({ instanceV1: build.query({ diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts index 1a0a9bd0b..c4560d79b 100644 --- a/web/source/settings/lib/types/domain-permission.ts +++ b/web/source/settings/lib/types/domain-permission.ts @@ -20,6 +20,7 @@ import typia from "typia"; import { PermType } from "./perm"; import { Links } from "parse-link-header"; +import { PermSubContentType } from "./permsubcontenttype"; export const validateDomainPerms = typia.createValidate(); @@ -213,3 +214,156 @@ export interface DomainPermExcludeCreateParams { */ private_comment?: string; } + +/** + * API model of one domain permission susbcription. + */ +export interface DomainPermSub { + /** + * The ID of the domain permission subscription. + */ + id: string; + /** + * The priority of the domain permission subscription. + */ + priority: number; + /** + * Time at which the subscription was created (ISO 8601 Datetime). + */ + created_at: string; + /** + * Title of this subscription, as set by admin who created or updated it. + */ + title: string; + /** + * The type of domain permission subscription (allow, block). + */ + permission_type: PermType; + /** + * If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. + * If false, domain permissions from this subscription will come into force immediately. + */ + as_draft: boolean; + /** + * If true, this domain permission subscription will "adopt" domain permissions + * which already exist on the instance, and which meet the following conditions: + * 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present + * in the subscribed list. Such orphaned domain permissions will be given this + * subscription's subscription ID value and be managed by this subscription. + */ + adopt_orphans: boolean; + /** + * ID of the account that created this subscription. + */ + created_by: string; + /** + * URI to call in order to fetch the permissions list. + */ + uri: string; + /** + * MIME content type to use when parsing the permissions list. + */ + content_type: PermSubContentType; + /** + * (Optional) username to set for basic auth when doing a fetch of URI. + */ + fetch_username?: string; + /** + * (Optional) password to set for basic auth when doing a fetch of URI. + */ + fetch_password?: string; + /** + * Time at which the most recent fetch was attempted (ISO 8601 Datetime). + */ + fetched_at?: string; + /** + * Time of the most recent successful fetch (ISO 8601 Datetime). + */ + successfully_fetched_at?: string; + /** + * If most recent fetch attempt failed, this field will contain an error message related to the fetch attempt. + */ + error?: string; + /** + * Count of domain permission entries discovered at URI on last (successful) fetch. + */ + count: number; +} + +/** + * Parameters for GET to /api/v1/admin/domain_permission_subscriptions. + */ +export interface DomainPermSubSearchParams { + /** + * Return only block or allow subscriptions. + */ + permission_type?: PermType; + /** + * Return only items *OLDER* than the given max ID (for paging downwards). + * The item with the specified ID will not be included in the response. + */ + max_id?: string; + /** + * Return only items *NEWER* than the given since ID. + * The item with the specified ID will not be included in the response. + */ + since_id?: string; + /** + * Return only items immediately *NEWER* than the given min ID (for paging upwards). + * The item with the specified ID will not be included in the response. + */ + min_id?: string; + /** + * Number of items to return. + */ + limit?: number; +} + +export interface DomainPermSubCreateUpdateParams { + /** + * The priority of the domain permission subscription. + */ + priority?: number; + /** + * Title of this subscription, as set by admin who created or updated it. + */ + title?: string; + /** + * URI to call in order to fetch the permissions list. + */ + uri: string; + /** + * MIME content type to use when parsing the permissions list. + */ + content_type: PermSubContentType; + /** + * If true, domain permissions arising from this subscription will be created as drafts that must be approved by a moderator to take effect. + * If false, domain permissions from this subscription will come into force immediately. + */ + as_draft?: boolean; + /** + * If true, this domain permission subscription will "adopt" domain permissions + * which already exist on the instance, and which meet the following conditions: + * 1) they have no subscription ID (ie., they're "orphaned") and 2) they are present + * in the subscribed list. Such orphaned domain permissions will be given this + * subscription's subscription ID value and be managed by this subscription. + */ + adopt_orphans?: boolean; + /** + * (Optional) username to set for basic auth when doing a fetch of URI. + */ + fetch_username?: string; + /** + * (Optional) password to set for basic auth when doing a fetch of URI. + */ + fetch_password?: string; + /** + * The type of domain permission subscription to create or update (allow, block). + */ + permission_type: PermType; +} + +export interface DomainPermSubSearchResp { + subs: DomainPermSub[]; + links: Links | null; +} diff --git a/web/source/settings/lib/types/permsubcontenttype.ts b/web/source/settings/lib/types/permsubcontenttype.ts new file mode 100644 index 000000000..0468aae4d --- /dev/null +++ b/web/source/settings/lib/types/permsubcontenttype.ts @@ -0,0 +1,20 @@ +/* + 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 . +*/ + +export type PermSubContentType = "text/plain" | "text/csv" | "application/json"; diff --git a/web/source/settings/lib/util/formvalidators.ts b/web/source/settings/lib/util/formvalidators.ts index c509cf59d..358db616c 100644 --- a/web/source/settings/lib/util/formvalidators.ts +++ b/web/source/settings/lib/util/formvalidators.ts @@ -46,3 +46,22 @@ export function formDomainValidator(domain: string): string { return "invalid domain"; } + +export function urlValidator(urlStr: string): string { + if (urlStr.length === 0) { + return ""; + } + + let url: URL; + try { + url = new URL(urlStr); + } catch (e) { + return e.message; + } + + if (url.protocol !== "http:" && url.protocol !== "https:") { + return `invalid protocol, must be http or https`; + } + + return formDomainValidator(url.host); +} diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 740c30059..bbb8fd61c 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1360,9 +1360,12 @@ button.tab-button { } .domain-permission-drafts-view, -.domain-permission-excludes-view { +.domain-permission-excludes-view, +.domain-permission-subscriptions-view, +.domain-permission-subscriptions-preview { .domain-permission-draft, - .domain-permission-exclude { + .domain-permission-exclude, + .domain-permission-subscription { display: flex; flex-direction: column; flex-wrap: nowrap; @@ -1404,14 +1407,18 @@ button.tab-button { } .domain-permission-draft-details, -.domain-permission-exclude-details { +.domain-permission-exclude-details, +.domain-permission-subscription-details { .info-list { margin-top: 1rem; } } .domain-permission-drafts-view, -.domain-permission-draft-details { +.domain-permission-draft-details, +.domain-permission-subscriptions-view, +.domain-permission-subscription-details, +.domain-permission-subscriptions-preview { dd.permission-type { display: flex; gap: 0.35rem; @@ -1419,6 +1426,35 @@ button.tab-button { } } +.domain-permission-subscription-title { + font-size: 1.2rem; + font-weight: bold; +} + +.domain-permission-subscription-create, +.domain-permission-subscription-update { + gap: 1rem; + + .password-show-hide { + display: flex; + gap: 0.5rem; + + .form-field.text { + flex: 1; + } + + .password-show-hide-toggle { + font-size: 1rem; + line-height: 1.4rem; + align-self: flex-end; + } + } +} + +.domain-permission-subscription-remove { + gap: 1rem; +} + .instance-rules { list-style-position: inside; margin: 0; 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 . +*/ + +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. +
+ 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 ( + + Learn more about domain permission subscriptions (opens in a new tab) + + ); +} + +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 ( + { + // 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} + > +
+ { permSub.title !== "" && + + {title} + + } +
+
Priority:
+
{priority}
+
+
+
Permission type:
+
+ + {permType} +
+
+
+
URL:
+
{uri}
+
+
+
Content type:
+
{contentType}
+
+
+
Create as draft:
+
{yesOrNo(asDraft)}
+
+
+
Adopt orphans:
+
{yesOrNo(adoptOrphans)}
+
+
+
Last fetch attempt:
+
{fetchedAtStr}
+
+
+
Last successful fetch:
+
{successfullyFetchedAtStr}
+
+
+
Discovered {permType}s:
+
{count}
+
+
+
+ ); +} 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 . +*/ + +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 ( + + ); +} + +function DomainPermSubForm({ data: permSub }: { data: DomainPermSub }) { + const baseUrl = useBaseUrl(); + const backLocation: string = history.state?.backLocation ?? `~${baseUrl}/subscriptions/search`; + + return ( +
+

Domain Permission Subscription Detail

+ + + +
+ ); +} + +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 ( +
+
+
Permission type:
+
+ + {permType} +
+
+
+
ID
+
{permSub.id}
+
+
+
Created
+
+
+
+
Created By
+
+ +
+
+
+
Last fetch attempt:
+
{fetchedAtStr}
+
+
+
Last successful fetch:
+
{successfullyFetchedAtStr}
+
+
+
Discovered {permSub.permission_type}s:
+
{permSub.count}
+
+
+ ); +} + +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 ( +
+

Edit Subscription

+ + + + + + + + + + + + } + > + + + + No subscriptions found that match your query.} + 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 . +*/ + +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 ( +
+
+

New Domain Permission Subscription

+

+ +
+ + + + + + + + + + + } + /> + + + <>Use + basic auth + <> when fetching + + } + field={useBasicAuth} + /> + + { useBasicAuth.value && + <> + +
+ + +
+ + } + + + + + + { !form.as_draft.value && +
+ + + Unchecking "create permissions as drafts" means that permissions found on the + subscribed list will be enforced immediately the next time the list is fetched. +
+ 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. +
+ 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. +
+
+ } + + + + ); +} 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 . +*/ + +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 ( +
+
+

Domain Permission Subscriptions Preview

+

+ You can use the form below to view through domain permission subscriptions sorted by priority (high to low). +
+ 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. +

+ +
+ +
+ ); +} + +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 ( + + ); + } + + return ( + <> +
+ +
+ No {permType.value}list subscriptions found.} + /> + + ); +} 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" /> + + + + + ); } 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() { + + + + -- cgit v1.2.3