aaaaaaaaaaaa

This commit is contained in:
tobi 2024-11-01 18:14:29 +01:00
parent e62f5f9dbc
commit 007bdfb232
15 changed files with 323 additions and 138 deletions

View file

@ -32,6 +32,7 @@
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// DomainPermissionDraftGet returns one
@ -174,7 +175,7 @@ func (p *Processor) DomainPermissionDraftAccept(
existing gtsmodel.DomainPermission
)
// Check if existing entry.
// Try to get existing entry.
switch permDraft.PermissionType {
case gtsmodel.DomainPermissionBlock:
existing, err = p.state.DB.GetDomainBlock(
@ -193,6 +194,15 @@ func (p *Processor) DomainPermissionDraftAccept(
return nil, "", gtserror.NewErrorInternalError(err)
}
// Check if we got existing entry.
existed := !util.IsNil(existing)
if existed && !overwrite {
// Domain permission exists and we shouldn't
// overwrite it, leave everything alone.
const text = "a domain permission already exists with this permission type and domain"
return nil, "", gtserror.NewErrorConflict(errors.New(text), text)
}
// Function to clean up the accepted draft, only called if
// creating or updating permission from draft is successful.
deleteDraft := func() {
@ -201,11 +211,9 @@ func (p *Processor) DomainPermissionDraftAccept(
}
}
switch {
// Easy case, we just need to create a new domain
// permission from the draft, and then delete it.
case existing == nil:
if !existed {
// Easy case, we just need to create a new domain
// permission from the draft, and then delete it.
var (
new *apimodel.DomainPermission
actionID string
@ -241,11 +249,10 @@ func (p *Processor) DomainPermissionDraftAccept(
deleteDraft()
return new, actionID, errWithCode
// Domain permission exists but we should overwrite
// it by just updating the existing domain permission.
// Domain can't change, so no need to re-run side effects.
case overwrite:
} else {
// Domain permission exists but we should overwrite
// it by just updating the existing domain permission.
// Domain can't change, so no need to re-run side effects.
existing.SetCreatedByAccountID(permDraft.CreatedByAccountID)
existing.SetCreatedByAccount(permDraft.CreatedByAccount)
existing.SetPrivateComment(permDraft.PrivateComment)
@ -273,13 +280,6 @@ func (p *Processor) DomainPermissionDraftAccept(
apiPerm, errWithCode := p.apiDomainPerm(ctx, existing, false)
return apiPerm, "", errWithCode
// Domain permission exists and we shouldn't
// overwrite it, leave everything alone.
default:
const text = "a domain permission already exists with this permission type and domain"
err := fmt.Errorf("%w: %s", err, text)
return nil, "", gtserror.NewErrorConflict(err, text)
}
}

View file

@ -17,6 +17,8 @@
package util
import "unsafe"
// EqualPtrs returns whether the values contained within two comparable ptr types are equal.
func EqualPtrs[T comparable](t1, t2 *T) bool {
switch {
@ -59,3 +61,8 @@ func PtrOrValue[T any](t *T, value T) T {
}
return value
}
func IsNil(i interface{}) bool {
type eface struct{ _, data unsafe.Pointer }
return (*eface)(unsafe.Pointer(&i)).data == nil
}

View file

@ -107,7 +107,11 @@ function Error({ error, reset }: ErrorProps) {
{ reset &&
<span
className="dismiss"
onClick={reset}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
reset();
}}
role="button"
tabIndex={0}
>

View file

@ -17,18 +17,107 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import React, { useEffect } from "react";
import { useLocation } from "wouter";
import { AdminAccount } from "../lib/types/account";
import { useLazyGetAccountQuery } from "../lib/query/admin";
import Loading from "./loading";
import { Error as ErrorC } from "./error";
interface UsernameProps {
interface UsernameLozengeProps {
/**
* Either an account ID (for fetching) or an account.
*/
account?: string | AdminAccount;
/**
* Make the lozenge clickable and link to this location.
*/
linkTo?: string;
/**
* Location to set as backLocation after linking to linkTo.
*/
backLocation?: string;
/**
* Additional classnames to add to the lozenge.
*/
classNames?: string[];
}
export default function UsernameLozenge({ account, linkTo, backLocation, classNames }: UsernameLozengeProps) {
if (account === undefined) {
return <>[unknown]</>;
} else if (typeof account === "string") {
return (
<FetchUsernameLozenge
accountID={account}
linkTo={linkTo}
backLocation={backLocation}
classNames={classNames}
/>
);
} else {
return (
<ReadyUsernameLozenge
account={account}
linkTo={linkTo}
backLocation={backLocation}
classNames={classNames}
/>
);
}
}
interface FetchUsernameLozengeProps {
accountID: string;
linkTo?: string;
backLocation?: string;
classNames?: string[];
}
function FetchUsernameLozenge({ accountID, linkTo, backLocation, classNames }: FetchUsernameLozengeProps) {
const [ trigger, result ] = useLazyGetAccountQuery();
// Call to get the account
// using the provided ID.
useEffect(() => {
trigger(accountID, true);
}, [trigger, accountID]);
const {
data: account,
isLoading,
isFetching,
isError,
error,
} = result;
// Wait for the account
// model to be returned.
if (isError) {
return <ErrorC error={error} />;
} else if (isLoading || isFetching || account === undefined) {
return <Loading />;
}
return (
<ReadyUsernameLozenge
account={account}
linkTo={linkTo}
backLocation={backLocation}
classNames={classNames}
/>
);
}
interface ReadyUsernameLozengeProps {
account: AdminAccount;
linkTo?: string;
backLocation?: string;
classNames?: string[];
}
export default function Username({ account, linkTo, backLocation, classNames }: UsernameProps) {
function ReadyUsernameLozenge({ account, linkTo, backLocation, classNames }: ReadyUsernameLozengeProps) {
const [ _location, setLocation ] = useLocation();
let className = "username-lozenge";

View file

@ -26,6 +26,7 @@ import type {
DomainPermDraftSearchResp,
} from "../../../types/domain-permission";
import parse from "parse-link-header";
import { PermType } from "../../../types/perm";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
@ -78,6 +79,63 @@ const extended = gtsApi.injectEndpoints({
}),
invalidatesTags: [{ type: "DomainPermissionDraft", id: "TRANSFORMED" }],
}),
acceptDomainPermissionDraft: build.mutation<DomainPerm, { id: string, overwrite?: boolean, permType: PermType }>({
query: ({ id, overwrite }) => ({
method: "POST",
url: `/api/v1/admin/domain_permission_drafts/${id}/accept`,
asForm: true,
body: {
overwrite: overwrite,
},
discardEmpty: true
}),
invalidatesTags: (res, _error, { id, permType }) => {
const invalidated: any[] = [];
// If error, nothing to invalidate.
if (!res) {
return invalidated;
}
// Invalidate this draft by ID, and
// the transformed list of all drafts.
invalidated.push(
{ type: 'DomainPermissionDraft', id: id },
{ type: "DomainPermissionDraft", id: "TRANSFORMED" },
);
// Invalidate cached blocks/allows depending
// on the permType of the accepted draft.
if (permType === "allow") {
invalidated.push("domainAllows");
} else {
invalidated.push("domainBlocks");
}
return invalidated;
}
}),
removeDomainPermissionDraft: build.mutation<DomainPerm, { id: string, ignore_target?: boolean }>({
query: ({ id, ignore_target }) => ({
method: "POST",
url: `/api/v1/admin/domain_permission_drafts/${id}/remove`,
asForm: true,
body: {
ignore_target: ignore_target,
},
discardEmpty: true
}),
invalidatesTags: (res, _error, { id }) =>
res
? [
{ type: "DomainPermissionDraft", id },
{ type: "DomainPermissionDraft", id: "TRANSFORMED" },
]
: [],
})
}),
});
@ -96,8 +154,20 @@ const useGetDomainPermissionDraftQuery = extended.useGetDomainPermissionDraftQue
*/
const useCreateDomainPermissionDraftMutation = extended.useCreateDomainPermissionDraftMutation;
/**
* Accept a domain permission draft, turning it into an enforced domain permission.
*/
const useAcceptDomainPermissionDraftMutation = extended.useAcceptDomainPermissionDraftMutation;
/**
* Remove a domain permission draft, optionally ignoring all future drafts targeting the given domain.
*/
const useRemoveDomainPermissionDraftMutation = extended.useRemoveDomainPermissionDraftMutation;
export {
useLazySearchDomainPermissionDraftsQuery,
useGetDomainPermissionDraftQuery,
useCreateDomainPermissionDraftMutation,
useAcceptDomainPermissionDraftMutation,
useRemoveDomainPermissionDraftMutation,
};

View file

@ -41,3 +41,16 @@ export function UseOurInstanceAccount(account: AdminAccount): boolean {
return !account.domain && account.username == ourDomain;
}
/**
* Uppercase first letter of given string.
*/
export function useCapitalize(i?: string): string {
return useMemo(() => {
if (i === undefined) {
return "";
}
return i.charAt(0).toUpperCase() + i.slice(1);
}, [i]);
}

View file

@ -1364,6 +1364,7 @@ button.tab-button {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
gap: 0.5rem;
&.block {
border-left: 0.3rem solid $error3;
@ -1385,6 +1386,18 @@ button.tab-button {
padding: 0;
}
}
.action-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
> .mutation-button
> button {
font-size: 1rem;
line-height: 1rem;
}
}
}
}

View file

@ -17,7 +17,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useMemo } from "react";
import React, { useMemo } from "react";
import { useLocation, useParams } from "wouter";
import { PermType } from "../../../lib/types/perm";
import { useDeleteHeaderAllowMutation, useDeleteHeaderBlockMutation, useGetHeaderAllowQuery, useGetHeaderBlockQuery } from "../../../lib/query/admin/http-header-permissions";
@ -26,8 +26,7 @@ import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { SerializedError } from "@reduxjs/toolkit";
import Loading from "../../../components/loading";
import { Error } from "../../../components/error";
import { useLazyGetAccountQuery } from "../../../lib/query/admin";
import Username from "../../../components/username";
import UsernameLozenge from "../../../components/username-lozenge";
import { useBaseUrl } from "../../../lib/navigation/util";
import BackButton from "../../../components/back-button";
import MutationButton from "../../../components/form/mutation-button";
@ -92,58 +91,19 @@ interface PermDeetsProps {
function PermDeets({
permType,
data: perm,
isLoading: isLoadingPerm,
isFetching: isFetchingPerm,
isError: isErrorPerm,
error: errorPerm,
isLoading,
isFetching,
isError,
error,
}: PermDeetsProps) {
const [ location ] = useLocation();
const baseUrl = useBaseUrl();
// Once we've loaded the perm, trigger
// getting the account that created it.
const [ getAccount, getAccountRes ] = useLazyGetAccountQuery();
useEffect(() => {
if (!perm) {
return;
}
getAccount(perm.created_by, true);
}, [getAccount, perm]);
// Load the createdByAccount if possible,
// returning a username lozenge with
// a link to the account.
const createdByAccount = useMemo(() => {
const {
data: account,
isLoading: isLoadingAccount,
isFetching: isFetchingAccount,
isError: isErrorAccount,
} = getAccountRes;
// Wait for query to finish, returning
// loading spinner in the meantime.
if (isLoadingAccount || isFetchingAccount || !perm) {
return <Loading />;
} else if (isErrorAccount || account === undefined) {
// Fall back to account ID.
return perm?.created_by;
}
return (
<Username
account={account}
linkTo={`~/settings/moderation/accounts/${account.id}`}
backLocation={`~${baseUrl}${location}`}
/>
);
}, [getAccountRes, perm, baseUrl, location]);
// Now wait til the perm itself is loaded.
if (isLoadingPerm || isFetchingPerm) {
// Wait til the perm itself is loaded.
if (isLoading || isFetching) {
return <Loading />;
} else if (isErrorPerm) {
return <Error error={errorPerm} />;
} else if (isError) {
return <Error error={error} />;
} else if (perm === undefined) {
throw "perm undefined";
}
@ -172,7 +132,13 @@ function PermDeets({
</div>
<div className="info-list-entry">
<dt>Created By</dt>
<dd>{createdByAccount}</dd>
<dd>
<UsernameLozenge
account={perm.created_by}
linkTo={`~/settings/moderation/accounts/${perm.created_by}`}
backLocation={`~${baseUrl}${location}`}
/>
</dd>
</div>
<div className="info-list-entry">
<dt>Header Name</dt>

View file

@ -21,7 +21,7 @@ import React, { ReactNode } from "react";
import { useSearchAccountsQuery } from "../../../../lib/query/admin";
import { PageableList } from "../../../../components/pageable-list";
import { useLocation } from "wouter";
import Username from "../../../../components/username";
import UsernameLozenge from "../../../../components/username-lozenge";
import { AdminAccount } from "../../../../lib/types/account";
export default function AccountsPending() {
@ -32,7 +32,7 @@ export default function AccountsPending() {
function itemToEntry(account: AdminAccount): ReactNode {
const acc = account.account;
return (
<Username
<UsernameLozenge
key={acc.acct}
account={account}
linkTo={`/${account.id}`}

View file

@ -26,7 +26,7 @@ import { Select, TextInput } from "../../../../components/form/inputs";
import MutationButton from "../../../../components/form/mutation-button";
import { useLocation, useSearch } from "wouter";
import { AdminAccount } from "../../../../lib/types/account";
import Username from "../../../../components/username";
import UsernameLozenge from "../../../../components/username-lozenge";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
export function AccountSearchForm() {
@ -93,7 +93,7 @@ export function AccountSearchForm() {
function itemToEntry(account: AdminAccount): ReactNode {
const acc = account.account;
return (
<Username
<UsernameLozenge
key={acc.acct}
account={account}
linkTo={`/${account.id}`}

View file

@ -17,15 +17,14 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, { useEffect, useMemo } from "react";
import React from "react";
import { useParams } from "wouter";
import Loading from "../../../../components/loading";
import { useBaseUrl } from "../../../../lib/navigation/util";
import BackButton from "../../../../components/back-button";
import { useGetDomainPermissionDraftQuery } from "../../../../lib/query/admin/domain-permissions/drafts";
import { Error as ErrorC } from "../../../../components/error";
import Username from "../../../../components/username";
import { useLazyGetAccountQuery } from "../../../../lib/query/admin";
import UsernameLozenge from "../../../../components/username-lozenge";
export default function DomainPermissionDraftDetail() {
const baseUrl = useBaseUrl();
@ -45,50 +44,6 @@ export default function DomainPermissionDraftDetail() {
error,
} = useGetDomainPermissionDraftQuery(draftID);
// Once we've triggered loading the perm draft,
// trigger getting the account that created it.
const [ getAccount, getAccountRes ] = useLazyGetAccountQuery();
useEffect(() => {
if (!permDraft) {
return;
}
if (!permDraft.created_by) {
return;
}
getAccount(permDraft.created_by, true);
}, [getAccount, permDraft]);
// Load the createdByAccount if possible,
// returning a username lozenge with
// a link to the account.
const createdByAccount = useMemo(() => {
const {
data: account,
isLoading: isLoadingAccount,
isFetching: isFetchingAccount,
isError: isErrorAccount,
} = getAccountRes;
// Wait for query to finish, returning
// loading spinner in the meantime.
if (isLoadingAccount || isFetchingAccount || !permDraft) {
return <Loading />;
} else if (isErrorAccount || account === undefined) {
// Fall back to account ID.
return permDraft?.created_by;
}
return (
<Username
account={account}
linkTo={`~/settings/moderation/accounts/${account.id}`}
backLocation={`~${location}`}
/>
);
}, [getAccountRes, permDraft]);
if (isLoading || isFetching) {
return <Loading />;
} else if (isError) {
@ -117,7 +72,13 @@ export default function DomainPermissionDraftDetail() {
</div>
<div className="info-list-entry">
<dt>Created By</dt>
<dd>{createdByAccount}</dd>
<dd>
<UsernameLozenge
account={permDraft.created_by}
linkTo={`~/settings/moderation/accounts/${permDraft.created_by}`}
backLocation={`~${location}`}
/>
</dd>
</div>
<div className="info-list-entry">
<dt>Domain</dt>

View file

@ -23,11 +23,12 @@ import { useTextInput } from "../../../../lib/form";
import { PageableList } from "../../../../components/pageable-list";
import MutationButton from "../../../../components/form/mutation-button";
import { useLocation, useSearch } from "wouter";
import { useLazySearchDomainPermissionDraftsQuery } from "../../../../lib/query/admin/domain-permissions/drafts";
import { useAcceptDomainPermissionDraftMutation, useLazySearchDomainPermissionDraftsQuery, useRemoveDomainPermissionDraftMutation } from "../../../../lib/query/admin/domain-permissions/drafts";
import { DomainPerm } from "../../../../lib/types/domain-permission";
import { Error as ErrorC } from "../../../../components/error";
import { Select, TextInput } from "../../../../components/form/inputs";
import { formDomainValidator } from "../../../../lib/util/formvalidators";
import { useCapitalize } from "../../../../lib/util";
export default function DomainPermissionDraftsSearch() {
return (
@ -190,20 +191,31 @@ interface DraftEntryProps {
function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) {
const [ _location, setLocation ] = useLocation();
const [ accept, acceptResult ] = useAcceptDomainPermissionDraftMutation();
const [ remove, removeResult ] = useRemoveDomainPermissionDraftMutation();
const domain = permDraft.domain;
const permType = permDraft.permission_type;
const permTypeUpper = useCapitalize(permType);
if (!permType) {
return <ErrorC error={new Error("permission_type was undefined")} />;
}
const publicComment = permDraft.public_comment ?? "[none]";
const privateComment = permDraft.private_comment ?? "[none]";
const subscriptionID = permDraft.subscription_id ?? "[none]";
const id = permDraft.id;
if (!id) {
return <ErrorC error={new Error("id was undefined")} />;
}
const title = `${permTypeUpper} ${domain}`;
return (
<span
className={`pseudolink domain-permission-draft entry ${permType}`}
// aria-label={title}
// title={title}
aria-label={title}
title={title}
onClick={() => {
// When clicking on a draft, direct
// to the detail view for that draft.
@ -217,6 +229,7 @@ function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) {
role="link"
tabIndex={0}
>
<h3>{title}</h3>
<dl className="info-list">
<div className="info-list-entry">
<dt>Domain:</dt>
@ -236,11 +249,45 @@ function DraftListEntry({ permDraft, linkTo, backLocation }: DraftEntryProps) {
<dt>Private comment:</dt>
<dd className="text-cutoff">{privateComment}</dd>
</div>
<div className="info-list-entry">
<dt>Public comment:</dt>
<dd>{publicComment}</dd>
</div>
<div className="info-list-entry">
<dt>Subscription:</dt>
<dd className="text-cutoff">{subscriptionID}</dd>
</div>
</dl>
<div className="action-buttons">
<MutationButton
label={`Accept ${permType}`}
title={`Accept ${permType}`}
type="button"
className="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
accept({ id, permType });
}}
disabled={false}
showError={true}
result={acceptResult}
/>
<MutationButton
label={`Remove draft`}
title={`Remove draft`}
type="button"
className="button danger"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
remove({ id });
}}
disabled={false}
showError={true}
result={removeResult}
/>
</div>
</span>
);
}

View file

@ -24,8 +24,11 @@ import { useBoolInput, useRadioInput, useTextInput } from "../../../../lib/form"
import { formDomainValidator } from "../../../../lib/util/formvalidators";
import MutationButton from "../../../../components/form/mutation-button";
import { Checkbox, RadioGroup, TextArea, TextInput } from "../../../../components/form/inputs";
import { useLocation } from "wouter";
export default function DomainPermissionDraftNew() {
const [ _location, setLocation ] = useLocation();
const form = {
domain: useTextInput("domain", {
validator: formDomainValidator,
@ -40,9 +43,21 @@ export default function DomainPermissionDraftNew() {
public_comment: useTextInput("public_comment"),
private_comment: useTextInput("private_comment"),
};
const [formSubmit, result] = useFormSubmit(form, useCreateDomainPermissionDraftMutation());
const [formSubmit, result] = useFormSubmit(
form,
useCreateDomainPermissionDraftMutation(),
{
changedOnly: false,
onFinish: (res) => {
if (res.data) {
// Creation successful,
// redirect to drafts overview.
setLocation(`/drafts/search`);
}
},
});
return (
<form
onSubmit={formSubmit}

View file

@ -25,7 +25,7 @@ import { useValue, useTextInput } from "../../../lib/form";
import useFormSubmit from "../../../lib/form/submit";
import { TextArea } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
import Username from "../../../components/username";
import UsernameLozenge from "../../../components/username-lozenge";
import { useGetReportQuery, useResolveReportMutation } from "../../../lib/query/admin/reports";
import { useBaseUrl } from "../../../lib/navigation/util";
import { AdminReport } from "../../../lib/types/report";
@ -99,7 +99,7 @@ function ReportBasicInfo({ report, baseUrl, location }: ReportSectionProps) {
<div className="info-list-entry">
<dt>Reported account</dt>
<dd>
<Username
<UsernameLozenge
account={target}
linkTo={`~/settings/moderation/accounts/${target.id}`}
backLocation={`~${baseUrl}${location}`}
@ -110,7 +110,7 @@ function ReportBasicInfo({ report, baseUrl, location }: ReportSectionProps) {
<div className="info-list-entry">
<dt>Reported by</dt>
<dd>
<Username
<UsernameLozenge
account={from}
linkTo={`~/settings/moderation/accounts/${from.id}`}
backLocation={`~${baseUrl}${location}`}
@ -173,7 +173,7 @@ function ReportHistory({ report, baseUrl, location }: ReportSectionProps) {
<div className="info-list-entry">
<dt>Handled by</dt>
<dd>
<Username
<UsernameLozenge
account={handled_by}
linkTo={`~/settings/moderation/accounts/${handled_by.id}`}
backLocation={`~${baseUrl}${location}`}

View file

@ -25,7 +25,7 @@ import { PageableList } from "../../../components/pageable-list";
import { Select } from "../../../components/form/inputs";
import MutationButton from "../../../components/form/mutation-button";
import { useLocation, useSearch } from "wouter";
import Username from "../../../components/username";
import UsernameLozenge from "../../../components/username-lozenge";
import { AdminReport } from "../../../lib/types/report";
export default function ReportsSearch() {
@ -206,7 +206,7 @@ function ReportListEntry({ report, linkTo, backLocation }: ReportEntryProps) {
<div className="info-list-entry">
<dt>Reported account:</dt>
<dd className="text-cutoff">
<Username
<UsernameLozenge
account={target}
classNames={["text-cutoff report-byline"]}
/>
@ -216,7 +216,7 @@ function ReportListEntry({ report, linkTo, backLocation }: ReportEntryProps) {
<div className="info-list-entry">
<dt>Reported by:</dt>
<dd className="text-cutoff reported-by">
<Username account={from} />
<UsernameLozenge account={from} />
</dd>
</div>