[feature] Allow import/export/creation of domain allows via admin panel (#2264)

* it's happening!

* aaa

* fix silly whoopsie

* it's working pa! it's working ma!

* model report parameters

* shuffle some more stuff around

* getting there

* oo hoo

* finish tidying up for now

* aaa

* fix use form submit errors

* peepee poo poo

* aaaaa

* ffff

* they see me typin', they hatin'

* boop

* aaa

* oooo

* typing typing tappa tappa

* almost done typing

* weee

* alright

* push it push it real good doo doo doo doo doo doo

* thingy no worky

* almost done

* mutation modifers not quite right

* hmm

* it works

* view blocks + allows nicely

* it works!

* typia install

* the old linterino

* linter plz
This commit is contained in:
tobi 2023-10-17 12:46:06 +02:00 committed by GitHub
parent 48725f7228
commit 637f188ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 4154 additions and 1690 deletions

View File

@ -54,6 +54,7 @@ steps:
path: /tmp/cache
commands:
- yarn --cwd ./web/source install --frozen-lockfile --cache-folder /tmp/cache
- yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup
- name: web-lint
image: node:18-alpine
@ -191,6 +192,6 @@ steps:
---
kind: signature
hmac: c3efbd528a76016562f88ae435141cfb5fd6d4d07b6ad2a24ecc23cb529cc1c6
hmac: d7b93470276a0df7e4d862941489f00da107df3d085200009b776d33599e6043
...

View File

@ -8,6 +8,7 @@ before:
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
# Install web deps + bundle web assets
- yarn --cwd ./web/source install
- yarn --cwd ./web/source ts-patch install # https://typia.io/docs/setup/#manual-setup
- yarn --cwd ./web/source build
builds:
# https://goreleaser.com/customization/build/

View File

@ -229,13 +229,15 @@ Using [NVM](https://github.com/nvm-sh/nvm) is one convenient way to install them
To install frontend dependencies:
```bash
yarn --cwd web/source
yarn --cwd ./web/source install && yarn --cwd ./web/source ts-patch install
```
The `ts-patch` step is necessary because of Typia, which we use for some type validation: see [Typia install docs](https://typia.io/docs/setup/#manual-setup).
To recompile frontend bundles into `web/assets/dist`:
```bash
yarn --cwd web/source build
yarn --cwd ./web/source build
```
#### Live Reloading

View File

@ -16,6 +16,7 @@ FROM --platform=${BUILDPLATFORM} node:18-alpine AS bundler
COPY web web
RUN yarn --cwd ./web/source install && \
yarn --cwd ./web/source ts-patch install && \
yarn --cwd ./web/source build && \
rm -rf ./web/source

View File

@ -95,7 +95,7 @@ func (m *Module) createDomainPermissions(
if importing && form.Domains.Size == 0 {
err = errors.New("import was specified but list of domains is empty")
} else if form.Domain == "" {
} else if !importing && form.Domain == "" {
err = errors.New("empty domain provided")
}

View File

@ -45,6 +45,10 @@
"@browserify/envify": "^6.0.0",
"@browserify/uglifyify": "^6.0.0",
"@joepie91/eslint-config": "^1.1.1",
"@types/bluebird": "^3.5.39",
"@types/is-valid-domain": "^0.0.2",
"@types/papaparse": "^5.3.9",
"@types/psl": "^1.1.1",
"@types/react-dom": "^18.2.8",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
@ -63,7 +67,10 @@
"postcss-nested": "^6.0.0",
"source-map-loader": "^4.0.1",
"ts-loader": "^9.4.4",
"ts-node": "^10.9.1",
"ts-patch": "^3.0.2",
"tsify": "^5.0.4",
"typescript": "^5.2.2"
"typescript": "^5.2.2",
"typia": "^5.1.6"
}
}

View File

@ -22,13 +22,13 @@ const { useRoute, Redirect } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
const FakeProfile = require("../../components/fake-profile");
const MutationButton = require("../../components/form/mutation-button");
const useFormSubmit = require("../../lib/form/submit");
const useFormSubmit = require("../../lib/form/submit").default;
const { useValue, useTextInput } = require("../../lib/form");
const { TextInput } = require("../../components/form/inputs");
@ -77,7 +77,7 @@ function AccountDetailForm({ data: account }) {
function ModifyAccount({ account }) {
const form = {
id: useValue("id", account.id),
reason: useTextInput("text", {})
reason: useTextInput("text")
};
const [modifyAccount, result] = useFormSubmit(form, query.useActionAccountMutation());

View File

@ -0,0 +1,254 @@
/*
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 from "react";
import { useMemo } from "react";
import { useLocation } from "wouter";
import { useTextInput, useBoolInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
import { TextInput, Checkbox, TextArea } from "../../components/form/inputs";
import Loading from "../../components/loading";
import BackButton from "../../components/back-button";
import MutationButton from "../../components/form/mutation-button";
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
import { useAddDomainAllowMutation, useAddDomainBlockMutation, useRemoveDomainAllowMutation, useRemoveDomainBlockMutation } from "../../lib/query/admin/domain-permissions/update";
import { DomainPerm, PermType } from "../../lib/types/domain-permission";
import { NoArg } from "../../lib/types/query";
import { Error } from "../../components/error";
export interface DomainPermDetailProps {
baseUrl: string;
permType: PermType;
domain: string;
}
export default function DomainPermDetail({ baseUrl, permType, domain }: DomainPermDetailProps) {
const { data: domainBlocks = {}, isLoading: isLoadingDomainBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
const { data: domainAllows = {}, isLoading: isLoadingDomainAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
let isLoading;
switch (permType) {
case "block":
isLoading = isLoadingDomainBlocks;
break;
case "allow":
isLoading = isLoadingDomainAllows;
break;
default:
throw "perm type unknown";
}
if (domain == "view") {
// Retrieve domain from form field submission.
domain = (new URL(document.location.toString())).searchParams.get("domain")?? "unknown";
}
if (domain == "unknown") {
throw "unknown domain";
}
// Normalize / decode domain (it may be URL-encoded).
domain = decodeURIComponent(domain);
// Check if we already have a perm of the desired type for this domain.
const existingPerm: DomainPerm | undefined = useMemo(() => {
if (permType == "block") {
return domainBlocks[domain];
} else {
return domainAllows[domain];
}
}, [domainBlocks, domainAllows, domain, permType]);
let infoContent: React.JSX.Element;
if (isLoading) {
infoContent = <Loading />;
} else if (existingPerm == undefined) {
infoContent = <span>No stored {permType} yet, you can add one below:</span>;
} else {
infoContent = (
<div className="info">
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
<b>Editing domain permissions isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
</div>
);
}
return (
<div>
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Domain {permType} for: <span title={domain}>{domain}</span></h1>
{infoContent}
<DomainPermForm
defaultDomain={domain}
perm={existingPerm}
permType={permType}
baseUrl={baseUrl}
/>
</div>
);
}
interface DomainPermFormProps {
defaultDomain: string;
perm?: DomainPerm;
permType: PermType;
baseUrl: string;
}
function DomainPermForm({ defaultDomain, perm, permType, baseUrl }: DomainPermFormProps) {
const isExistingPerm = perm !== undefined;
const disabledForm = isExistingPerm
? {
disabled: true,
title: "Domain permissions currently cannot be edited."
}
: {
disabled: false,
title: "",
};
const form = {
domain: useTextInput("domain", { source: perm, defaultValue: defaultDomain }),
obfuscate: useBoolInput("obfuscate", { source: perm }),
commentPrivate: useTextInput("private_comment", { source: perm }),
commentPublic: useTextInput("public_comment", { source: perm })
};
// Check which perm type we're meant to be handling
// here, and use appropriate mutations and results.
// We can't call these hooks conditionally because
// react is like "weh" (mood), but we can decide
// which ones to use conditionally.
const [ addBlock, addBlockResult ] = useAddDomainBlockMutation();
const [ removeBlock, removeBlockResult] = useRemoveDomainBlockMutation({ fixedCacheKey: perm?.id });
const [ addAllow, addAllowResult ] = useAddDomainAllowMutation();
const [ removeAllow, removeAllowResult ] = useRemoveDomainAllowMutation({ fixedCacheKey: perm?.id });
const [
addTrigger,
addResult,
removeTrigger,
removeResult,
] = useMemo(() => {
return permType == "block"
? [
addBlock,
addBlockResult,
removeBlock,
removeBlockResult,
]
: [
addAllow,
addAllowResult,
removeAllow,
removeAllowResult,
];
}, [permType,
addBlock, addBlockResult, removeBlock, removeBlockResult,
addAllow, addAllowResult, removeAllow, removeAllowResult,
]);
// Use appropriate submission params for this permType.
const [submitForm, submitFormResult] = useFormSubmit(form, [addTrigger, addResult], { changedOnly: false });
// Uppercase first letter of given permType.
const permTypeUpper = useMemo(() => {
return permType.charAt(0).toUpperCase() + permType.slice(1);
}, [permType]);
const [location, setLocation] = useLocation();
function verifyUrlThenSubmit(e) {
// Adding a new domain permissions happens on a url like
// "/settings/admin/domain-permissions/:permType/domain.com",
// but if domain input changes, that doesn't match anymore
// and causes issues later on so, before submitting the form,
// silently change url, and THEN submit.
let correctUrl = `${baseUrl}/${form.domain.value}`;
if (location != correctUrl) {
setLocation(correctUrl);
}
return submitForm(e);
}
return (
<form onSubmit={verifyUrlThenSubmit}>
<TextInput
field={form.domain}
label="Domain"
placeholder="example.com"
{...disabledForm}
/>
<Checkbox
field={form.obfuscate}
label="Obfuscate domain in public lists"
{...disabledForm}
/>
<TextArea
field={form.commentPrivate}
label="Private comment"
rows={3}
{...disabledForm}
/>
<TextArea
field={form.commentPublic}
label="Public comment"
rows={3}
{...disabledForm}
/>
<div className="action-buttons row">
<MutationButton
label={permTypeUpper}
result={submitFormResult}
showError={false}
{...disabledForm}
/>
{
isExistingPerm &&
<MutationButton
type="button"
onClick={() => removeTrigger(perm.id?? "")}
label="Remove"
result={removeResult}
className="button danger"
showError={false}
disabled={!isExistingPerm}
/>
}
</div>
<>
{addResult.error && <Error error={addResult.error} />}
{removeResult.error && <Error error={removeResult.error} />}
</>
</form>
);
}

View File

@ -17,34 +17,57 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
const useFormSubmit = require("../../../lib/form/submit");
import { useEffect } from "react";
const {
import { useExportDomainListMutation } from "../../lib/query/admin/domain-permissions/export";
import useFormSubmit from "../../lib/form/submit";
import {
RadioGroup,
TextArea,
Select,
} = require("../../../components/form/inputs");
} from "../../components/form/inputs";
const MutationButton = require("../../../components/form/mutation-button");
import MutationButton from "../../components/form/mutation-button";
const { Error } = require("../../../components/error");
const ExportFormatTable = require("./export-format-table");
import { Error } from "../../components/error";
import ExportFormatTable from "./export-format-table";
module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
const [submitExport, exportResult] = useFormSubmit(form, query.useExportDomainListMutation());
import type {
FormSubmitFunction,
FormSubmitResult,
RadioFormInputHook,
TextFormInputHook,
} from "../../lib/form/types";
export interface ImportExportFormProps {
form: {
domains: TextFormInputHook;
exportType: TextFormInputHook;
permType: RadioFormInputHook;
};
submitParse: FormSubmitFunction;
parseResult: FormSubmitResult;
}
export default function ImportExportForm({ form, submitParse, parseResult }: ImportExportFormProps) {
const [submitExport, exportResult] = useFormSubmit(form, useExportDomainListMutation());
function fileChanged(e) {
const reader = new FileReader();
reader.onload = function (read) {
form.domains.value = read.target.result;
submitParse();
const res = read.target?.result;
if (typeof res === "string") {
form.domains.value = res;
submitParse();
}
};
reader.readAsText(e.target.files[0]);
}
React.useEffect(() => {
useEffect(() => {
if (exportResult.isSuccess) {
form.domains.setter(exportResult.data);
}
@ -53,12 +76,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
return (
<>
<h1>Import / Export suspended domains</h1>
<p>
This page can be used to import and export lists of domains to suspend.
Exports can be done in various formats, with varying functionality and support in other software.
Imports will automatically detect what format is being processed.
</p>
<h1>Import / Export domain permissions</h1>
<p>This page can be used to import and export lists of domain permissions.</p>
<p>Exports can be done in various formats, with varying functionality and support in other software.</p>
<p>Imports will automatically detect what format is being processed.</p>
<ExportFormatTable />
<div className="import-export">
<TextArea
@ -68,6 +89,10 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
rows={8}
/>
<RadioGroup
field={form.permType}
/>
<div className="button-grid">
<MutationButton
label="Import"
@ -75,6 +100,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
onClick={() => submitParse()}
result={parseResult}
showError={false}
disabled={false}
/>
<label className="button with-icon">
<i className="fa fa-fw " aria-hidden="true" />
@ -92,6 +118,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
type="button"
onClick={() => submitExport("export")}
result={exportResult} showError={false}
disabled={false}
/>
<MutationButton
label="Export to file"
@ -100,6 +127,7 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
onClick={() => submitExport("export-file")}
result={exportResult}
showError={false}
disabled={false}
/>
<div className="export-file">
<span>
@ -121,4 +149,4 @@ module.exports = function ImportExportForm({ form, submitParse, parseResult }) {
</div>
</>
);
};
}

View File

@ -0,0 +1,90 @@
/*
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 from "react";
import { Switch, Route, Redirect, useLocation } from "wouter";
import { useProcessDomainPermissionsMutation } from "../../lib/query/admin/domain-permissions/process";
import { useTextInput, useRadioInput } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
import { ProcessImport } from "./process";
import ImportExportForm from "./form";
export default function ImportExport({ baseUrl }) {
const form = {
domains: useTextInput("domains"),
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true }),
permType: useRadioInput("permType", {
options: {
block: "Domain blocks",
allow: "Domain allows",
}
})
};
const [submitParse, parseResult] = useFormSubmit(form, useProcessDomainPermissionsMutation(), { changedOnly: false });
const [_location, setLocation] = useLocation();
return (
<Switch>
<Route path={`${baseUrl}/process`}>
{
parseResult.isSuccess
? (
<>
<h1>
<span
className="button"
onClick={() => {
parseResult.reset();
setLocation(baseUrl);
}}
>
&lt; back
</span>
&nbsp; Confirm import of domain {form.permType.value}s:
</h1>
<ProcessImport
list={parseResult.data}
permType={form.permType}
/>
</>
)
: <Redirect to={baseUrl} />
}
</Route>
<Route>
{
parseResult.isSuccess
? <Redirect to={`${baseUrl}/process`} />
: <ImportExportForm
form={form}
submitParse={submitParse}
parseResult={parseResult}
/>
}
</Route>
</Switch>
);
}

View File

@ -0,0 +1,49 @@
/*
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 from "react";
import { Switch, Route } from "wouter";
import DomainPermissionsOverview from "./overview";
import { PermType } from "../../lib/types/domain-permission";
import DomainPermDetail from "./detail";
export default function DomainPermissions({ baseUrl }: { baseUrl: string }) {
return (
<Switch>
<Route path="/settings/admin/domain-permissions/:permType/:domain">
{params => (
<DomainPermDetail
permType={params.permType as PermType}
baseUrl={baseUrl}
domain={params.domain}
/>
)}
</Route>
<Route path="/settings/admin/domain-permissions/:permType">
{params => (
<DomainPermissionsOverview
permType={params.permType as PermType}
baseUrl={baseUrl}
/>
)}
</Route>
</Switch>
);
}

View File

@ -0,0 +1,198 @@
/*
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 from "react";
import { useMemo } from "react";
import { Link, useLocation } from "wouter";
import { matchSorter } from "match-sorter";
import { useTextInput } from "../../lib/form";
import { TextInput } from "../../components/form/inputs";
import Loading from "../../components/loading";
import { useDomainAllowsQuery, useDomainBlocksQuery } from "../../lib/query/admin/domain-permissions/get";
import type { MappedDomainPerms, PermType } from "../../lib/types/domain-permission";
import { NoArg } from "../../lib/types/query";
export interface DomainPermissionsOverviewProps {
// Params injected by
// the wouter router.
permType: PermType;
baseUrl: string,
}
export default function DomainPermissionsOverview({ permType, baseUrl }: DomainPermissionsOverviewProps) {
if (permType !== "block" && permType !== "allow") {
throw "unrecognized perm type " + permType;
}
// Uppercase first letter of given permType.
const permTypeUpper = useMemo(() => {
return permType.charAt(0).toUpperCase() + permType.slice(1);
}, [permType]);
// Fetch / wait for desired perms to load.
const { data: blocks, isLoading: isLoadingBlocks } = useDomainBlocksQuery(NoArg, { skip: permType !== "block" });
const { data: allows, isLoading: isLoadingAllows } = useDomainAllowsQuery(NoArg, { skip: permType !== "allow" });
let data: MappedDomainPerms | undefined;
let isLoading: boolean;
if (permType == "block") {
data = blocks;
isLoading = isLoadingBlocks;
} else {
data = allows;
isLoading = isLoadingAllows;
}
if (isLoading || data === undefined) {
return <Loading />;
}
return (
<div>
<h1>Domain {permTypeUpper}s</h1>
{ permType == "block" ? <BlockHelperText/> : <AllowHelperText/> }
<DomainPermsList
data={data}
baseUrl={baseUrl}
permType={permType}
permTypeUpper={permTypeUpper}
/>
<Link to={`${baseUrl}/import-export`}>
<a>Or use the bulk import/export interface</a>
</Link>
</div>
);
}
interface DomainPermsListProps {
data: MappedDomainPerms;
baseUrl: string;
permType: PermType;
permTypeUpper: string;
}
function DomainPermsList({ data, baseUrl, permType, permTypeUpper }: DomainPermsListProps) {
// Format perms into a list.
const perms = useMemo(() => {
return Object.values(data);
}, [data]);
const [_location, setLocation] = useLocation();
const filterField = useTextInput("filter");
function filterFormSubmit(e) {
e.preventDefault();
setLocation(`${baseUrl}/${filter}`);
}
const filter = filterField.value ?? "";
const filteredPerms = useMemo(() => {
return matchSorter(perms, filter, { keys: ["domain"] });
}, [perms, filter]);
const filtered = perms.length - filteredPerms.length;
const filterInfo = (
<span>
{perms.length} {permType}ed domain{perms.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
</span>
);
const entries = filteredPerms.map((entry) => {
return (
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
<a className="entry nounderline">
<span id="domain">{entry.domain}</span>
<span id="date">{new Date(entry.created_at ?? "").toLocaleString()}</span>
</a>
</Link>
);
});
return (
<div className="domain-permissions-list">
<form className="filter" role="search" onSubmit={filterFormSubmit}>
<TextInput
field={filterField}
placeholder="example.org"
label={`Search or add domain ${permType}`}
/>
<Link to={`${baseUrl}/${filter}`}>
<a className="button">{permTypeUpper}&nbsp;{filter}</a>
</Link>
</form>
<div>
{filterInfo}
<div className="list">
<div className="entries scrolling">
{entries}
</div>
</div>
</div>
</div>
);
}
function BlockHelperText() {
return (
<p>
Blocking a domain blocks interaction between your instance, and all current and future accounts on
instance(s) running on the blocked domain. Stored content will be removed, and no more data is sent to
the remote server. This extends to all subdomains as well, so blocking 'example.com' also blocks 'social.example.com'.
<br/>
<a
href="https://docs.gotosocial.org/en/latest/admin/domain_blocks/"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about domain blocks (opens in a new tab)
</a>
<br/>
</p>
);
}
function AllowHelperText() {
return (
<p>
Allowing a domain explicitly allows instance(s) running on that domain to interact with your instance.
If you're running in allowlist mode, this is how you "allow" instances through.
If you're running in blocklist mode (the default federation mode), you can use explicit domain allows
to override domain blocks. In blocklist mode, explicitly allowed instances will be able to interact with
your instance regardless of any domain blocks in place. This extends to all subdomains as well, so allowing
'example.com' also allows 'social.example.com'. This is useful when you're importing a block list but
there are some domains on the list you don't want to block: just create an explicit allow for those domains
before importing the list.
<br/>
<a
href="https://docs.gotosocial.org/en/latest/admin/federation_modes/"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about federation modes (opens in a new tab)
</a>
</p>
);
}

View File

@ -17,57 +17,81 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
const query = require("../../../lib/query");
const { isValidDomainBlock, hasBetterScope } = require("../../../lib/domain-block");
import { memo, useMemo, useCallback, useEffect } from "react";
const {
import { isValidDomainPermission, hasBetterScope } from "../../lib/util/domain-permission";
import {
useTextInput,
useBoolInput,
useRadioInput,
useCheckListInput
} = require("../../../lib/form");
useCheckListInput,
} from "../../lib/form";
const useFormSubmit = require("../../../lib/form/submit");
const {
TextInput,
TextArea,
Checkbox,
import {
Select,
RadioGroup
} = require("../../../components/form/inputs");
TextArea,
RadioGroup,
Checkbox,
TextInput,
} from "../../components/form/inputs";
const CheckList = require("../../../components/check-list");
const MutationButton = require("../../../components/form/mutation-button");
const FormWithData = require("../../../lib/form/form-with-data");
import useFormSubmit from "../../lib/form/submit";
module.exports = React.memo(
function ProcessImport({ list }) {
import CheckList from "../../components/check-list";
import MutationButton from "../../components/form/mutation-button";
import FormWithData from "../../lib/form/form-with-data";
import { useImportDomainPermsMutation } from "../../lib/query/admin/domain-permissions/import";
import {
useDomainAllowsQuery,
useDomainBlocksQuery
} from "../../lib/query/admin/domain-permissions/get";
import type { DomainPerm, MappedDomainPerms } from "../../lib/types/domain-permission";
import type { ChecklistInputHook, RadioFormInputHook } from "../../lib/form/types";
export interface ProcessImportProps {
list: DomainPerm[],
permType: RadioFormInputHook,
}
export const ProcessImport = memo(
function ProcessImport({ list, permType }: ProcessImportProps) {
return (
<div className="without-border">
<FormWithData
dataQuery={query.useInstanceBlocksQuery}
dataQuery={permType.value == "allow"
? useDomainAllowsQuery
: useDomainBlocksQuery
}
DataForm={ImportList}
list={list}
{...{ list, permType }}
/>
</div>
);
}
);
function ImportList({ list, data: blockedInstances }) {
const hasComment = React.useMemo(() => {
export interface ImportListProps {
list: Array<DomainPerm>,
data: MappedDomainPerms,
permType: RadioFormInputHook,
}
function ImportList({ list, data: domainPerms, permType }: ImportListProps) {
const hasComment = useMemo(() => {
let hasPublic = false;
let hasPrivate = false;
list.some((entry) => {
if (entry.public_comment?.length > 0) {
if (entry.public_comment) {
hasPublic = true;
}
if (entry.private_comment?.length > 0) {
if (entry.private_comment) {
hasPrivate = true;
}
@ -88,7 +112,7 @@ function ImportList({ list, data: blockedInstances }) {
const showComment = useTextInput("showComment", { defaultValue: hasComment.type ?? "public_comment" });
const form = {
domains: useCheckListInput("domains", { entries: list }),
domains: useCheckListInput("domains", { entries: list }), // DomainPerm is actually also a Checkable.
obfuscate: useBoolInput("obfuscate"),
privateComment: useTextInput("private_comment", {
defaultValue: `Imported on ${new Date().toLocaleString()}`
@ -108,13 +132,17 @@ function ImportList({ list, data: blockedInstances }) {
replace: "Replace"
}
}),
permType: permType,
};
const [importDomains, importResult] = useFormSubmit(form, query.useImportDomainListMutation(), { changedOnly: false });
const [importDomains, importResult] = useFormSubmit(form, useImportDomainPermsMutation(), { changedOnly: false });
return (
<>
<form onSubmit={importDomains} className="suspend-import-list">
<form
onSubmit={importDomains}
className="domain-perm-import-list"
>
<span>{list.length} domain{list.length != 1 ? "s" : ""} in this list</span>
{hasComment.both &&
@ -129,8 +157,9 @@ function ImportList({ list, data: blockedInstances }) {
<div className="checkbox-list-wrapper">
<DomainCheckList
field={form.domains}
blockedInstances={blockedInstances}
commentType={showComment.value}
domainPerms={domainPerms}
commentType={showComment.value as "public_comment" | "private_comment"}
permType={form.permType}
/>
</div>
@ -159,28 +188,41 @@ function ImportList({ list, data: blockedInstances }) {
label="Obfuscate domains in public lists"
/>
<MutationButton label="Import" result={importResult} />
<MutationButton
label="Import"
disabled={false}
result={importResult}
/>
</form>
</>
);
}
function DomainCheckList({ field, blockedInstances, commentType }) {
const getExtraProps = React.useCallback((entry) => {
interface DomainCheckListProps {
field: ChecklistInputHook,
domainPerms: MappedDomainPerms,
commentType: "public_comment" | "private_comment",
permType: RadioFormInputHook,
}
function DomainCheckList({ field, domainPerms, commentType, permType }: DomainCheckListProps) {
const getExtraProps = useCallback((entry: DomainPerm) => {
return {
comment: entry[commentType],
alreadyExists: blockedInstances[entry.domain] != undefined
alreadyExists: entry.domain in domainPerms,
permType: permType,
};
}, [blockedInstances, commentType]);
}, [domainPerms, commentType, permType]);
const entriesWithSuggestions = React.useMemo(() => (
Object.values(field.value).filter((entry) => entry.suggest)
), [field.value]);
const entriesWithSuggestions = useMemo(() => {
const fieldValue = (field.value ?? {}) as { [k: string]: DomainPerm; };
return Object.values(fieldValue).filter((entry) => entry.suggest);
}, [field.value]);
return (
<>
<CheckList
field={field}
field={field as ChecklistInputHook}
header={<>
<b>Domain</b>
<b>
@ -200,8 +242,14 @@ function DomainCheckList({ field, blockedInstances, commentType }) {
);
}
const UpdateHint = React.memo(
function UpdateHint({ entries, updateEntry, updateMultiple }) {
interface UpdateHintProps {
entries,
updateEntry,
updateMultiple,
}
const UpdateHint = memo(
function UpdateHint({ entries, updateEntry, updateMultiple }: UpdateHintProps) {
if (entries.length == 0) {
return null;
}
@ -229,8 +277,13 @@ const UpdateHint = React.memo(
}
);
const UpdateableEntry = React.memo(
function UpdateableEntry({ entry, updateEntry }) {
interface UpdateableEntryProps {
entry,
updateEntry,
}
const UpdateableEntry = memo(
function UpdateableEntry({ entry, updateEntry }: UpdateableEntryProps) {
return (
<>
<span className="text-cutoff">{entry.domain}</span>
@ -248,21 +301,31 @@ function domainValidationError(isValid) {
return isValid ? "" : "Invalid domain";
}
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }) {
interface DomainEntryProps {
entry;
onChange;
extraProps: {
alreadyExists: boolean;
comment: string;
permType: RadioFormInputHook;
};
}
function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment, permType } }: DomainEntryProps) {
const domainField = useTextInput("domain", {
defaultValue: entry.domain,
showValidation: entry.checked,
initValidation: domainValidationError(entry.valid),
validator: (value) => domainValidationError(isValidDomainBlock(value))
validator: (value) => domainValidationError(isValidDomainPermission(value))
});
React.useEffect(() => {
useEffect(() => {
if (entry.valid != domainField.valid) {
onChange({ valid: domainField.valid });
}
}, [onChange, entry.valid, domainField.valid]);
React.useEffect(() => {
useEffect(() => {
if (entry.domain != domainField.value) {
domainField.setter(entry.domain);
}
@ -270,8 +333,8 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [entry.domain, domainField.setter]);
React.useEffect(() => {
onChange({ suggest: hasBetterScope(domainField.value) });
useEffect(() => {
onChange({ suggest: hasBetterScope(domainField.value ?? "") });
// only need this update if it's the entry.checked that updated, not onChange
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [domainField.value]);
@ -296,7 +359,11 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
}}
/>
<span id="icon" onClick={clickIcon}>
<DomainEntryIcon alreadyExists={alreadyExists} suggestion={entry.suggest} onChange={onChange} />
<DomainEntryIcon
alreadyExists={alreadyExists}
suggestion={entry.suggest}
permTypeString={permType.value?? ""}
/>
</span>
</div>
<p>{comment}</p>
@ -304,7 +371,13 @@ function DomainEntry({ entry, onChange, extraProps: { alreadyExists, comment } }
);
}
function DomainEntryIcon({ alreadyExists, suggestion }) {
interface DomainEntryIconProps {
alreadyExists: boolean;
suggestion: string;
permTypeString: string;
}
function DomainEntryIcon({ alreadyExists, suggestion, permTypeString }: DomainEntryIconProps) {
let icon;
let text;
@ -312,8 +385,8 @@ function DomainEntryIcon({ alreadyExists, suggestion }) {
icon = "fa-info-circle suggest-changes";
text = `Entry targets a specific subdomain, consider changing it to '${suggestion}'.`;
} else if (alreadyExists) {
icon = "fa-history already-blocked";
text = "Domain block already exists.";
icon = "fa-history permission-already-exists";
text = `Domain ${permTypeString} already exists.`;
}
if (!icon) {

View File

@ -22,9 +22,8 @@ const splitFilterN = require("split-filter-n");
const syncpipe = require('syncpipe');
const { matchSorter } = require("match-sorter");
const query = require("../../lib/query");
const ComboBox = require("../../components/combo-box");
const { useListEmojiQuery } = require("../../lib/query/admin/custom-emoji");
function useEmojiByCategory(emoji) {
// split all emoji over an object keyed by the category names (or Unsorted)
@ -43,7 +42,7 @@ function CategorySelect({ field, children }) {
isLoading,
isSuccess,
error
} = query.useListEmojiQuery({ filter: "domain:local" });
} = useListEmojiQuery({ filter: "domain:local" });
const emojiByCategory = useEmojiByCategory(emoji);

View File

@ -20,21 +20,25 @@
const React = require("react");
const { useRoute, Link, Redirect } = require("wouter");
const query = require("../../../lib/query");
const { useComboBoxInput, useFileInput, useValue } = require("../../../lib/form");
const { CategorySelect } = require("../category-select");
const useFormSubmit = require("../../../lib/form/submit");
const useFormSubmit = require("../../../lib/form/submit").default;
const { useBaseUrl } = require("../../../lib/navigation/util");
const FakeToot = require("../../../components/fake-toot");
const FormWithData = require("../../../lib/form/form-with-data");
const FormWithData = require("../../../lib/form/form-with-data").default;
const Loading = require("../../../components/loading");
const { FileInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
const {
useGetEmojiQuery,
useEditEmojiMutation,
useDeleteEmojiMutation,
} = require("../../../lib/query/admin/custom-emoji");
module.exports = function EmojiDetailRoute({ }) {
const baseUrl = useBaseUrl();
let [_match, params] = useRoute(`${baseUrl}/:emojiId`);
@ -44,7 +48,7 @@ module.exports = function EmojiDetailRoute({ }) {
return (
<div className="emoji-detail">
<Link to={baseUrl}><a>&lt; go back</a></Link>
<FormWithData dataQuery={query.useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
<FormWithData dataQuery={useGetEmojiQuery} queryArg={params.emojiId} DataForm={EmojiDetailForm} />
</div>
);
}
@ -61,7 +65,7 @@ function EmojiDetailForm({ data: emoji }) {
})
};
const [modifyEmoji, result] = useFormSubmit(form, query.useEditEmojiMutation());
const [modifyEmoji, result] = useFormSubmit(form, useEditEmojiMutation());
// Automatic submitting of category change
React.useEffect(() => {
@ -74,7 +78,7 @@ function EmojiDetailForm({ data: emoji }) {
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [form.category.hasChanged(), form.category.isNew, form.category.state.open]);
const [deleteEmoji, deleteResult] = query.useDeleteEmojiMutation();
const [deleteEmoji, deleteResult] = useDeleteEmojiMutation();
if (deleteResult.isSuccess) {
return <Redirect to={baseUrl} />;

View File

@ -19,15 +19,13 @@
const React = require("react");
const query = require("../../../lib/query");
const {
useFileInput,
useComboBoxInput
} = require("../../../lib/form");
const useShortcode = require("./use-shortcode");
const useFormSubmit = require("../../../lib/form/submit");
const useFormSubmit = require("../../../lib/form/submit").default;
const {
TextInput, FileInput
@ -36,11 +34,13 @@ const {
const { CategorySelect } = require('../category-select');
const FakeToot = require("../../../components/fake-toot");
const MutationButton = require("../../../components/form/mutation-button");
const { useAddEmojiMutation } = require("../../../lib/query/admin/custom-emoji");
const { useInstanceV1Query } = require("../../../lib/query");
module.exports = function NewEmojiForm() {
const shortcode = useShortcode();
const { data: instance } = query.useInstanceQuery();
const { data: instance } = useInstanceV1Query();
const emojiMaxSize = React.useMemo(() => {
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
}, [instance]);
@ -54,7 +54,7 @@ module.exports = function NewEmojiForm() {
const [submitForm, result] = useFormSubmit({
shortcode, image, category
}, query.useAddEmojiMutation());
}, useAddEmojiMutation());
React.useEffect(() => {
if (shortcode.value.length == 0) {

View File

@ -25,13 +25,13 @@ const { matchSorter } = require("match-sorter");
const NewEmojiForm = require("./new-emoji");
const { useTextInput } = require("../../../lib/form");
const query = require("../../../lib/query");
const { useEmojiByCategory } = require("../category-select");
const { useBaseUrl } = require("../../../lib/navigation/util");
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
const { TextInput } = require("../../../components/form/inputs");
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
module.exports = function EmojiOverview({ }) {
const {
@ -39,7 +39,7 @@ module.exports = function EmojiOverview({ }) {
isLoading,
isError,
error
} = query.useListEmojiQuery({ filter: "domain:local" });
} = useListEmojiQuery({ filter: "domain:local" });
let content = null;

View File

@ -19,15 +19,15 @@
const React = require("react");
const query = require("../../../lib/query");
const { useTextInput } = require("../../../lib/form");
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
const shortcodeRegex = /^\w{2,30}$/;
module.exports = function useShortcode() {
const {
data: emoji = []
} = query.useListEmojiQuery({ filter: "domain:local" });
const { data: emoji = [] } = useListEmojiQuery({
filter: "domain:local"
});
const emojiCodes = React.useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));

View File

@ -21,9 +21,9 @@ const React = require("react");
const ParseFromToot = require("./parse-from-toot");
const query = require("../../../lib/query");
const Loading = require("../../../components/loading");
const { Error } = require("../../../components/error");
const { useListEmojiQuery } = require("../../../lib/query/admin/custom-emoji");
module.exports = function RemoteEmoji() {
// local emoji are queried for shortcode collision detection
@ -31,7 +31,7 @@ module.exports = function RemoteEmoji() {
data: emoji = [],
isLoading,
error
} = query.useListEmojiQuery({ filter: "domain:local" });
} = useListEmojiQuery({ filter: "domain:local" });
const emojiCodes = React.useMemo(() => {
return new Set(emoji.map((e) => e.shortcode));

View File

@ -19,25 +19,27 @@
const React = require("react");
const query = require("../../../lib/query");
const {
useTextInput,
useComboBoxInput,
useCheckListInput
} = require("../../../lib/form");
const useFormSubmit = require("../../../lib/form/submit");
const useFormSubmit = require("../../../lib/form/submit").default;
const CheckList = require("../../../components/check-list");
const CheckList = require("../../../components/check-list").default;
const { CategorySelect } = require('../category-select');
const { TextInput } = require("../../../components/form/inputs");
const MutationButton = require("../../../components/form/mutation-button");
const { Error } = require("../../../components/error");
const {
useSearchItemForEmojiMutation,
usePatchRemoteEmojisMutation
} = require("../../../lib/query/admin/custom-emoji");
module.exports = function ParseFromToot({ emojiCodes }) {
const [searchStatus, result] = query.useSearchStatusForEmojiMutation();
const [searchStatus, result] = useSearchItemForEmojiMutation();
const [onURLChange, _resetURL, { url }] = useTextInput("url");
@ -121,7 +123,7 @@ function CopyEmojiForm({ localEmojiCodes, type, emojiList }) {
const [formSubmit, result] = useFormSubmit(
form,
query.usePatchRemoteEmojisMutation(),
usePatchRemoteEmojisMutation(),
{
changedOnly: false,
onFinish: ({ data }) => {

View File

@ -1,168 +0,0 @@
/*
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/>.
*/
const React = require("react");
const { useRoute, Redirect, useLocation } = require("wouter");
const query = require("../../lib/query");
const { useTextInput, useBoolInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit");
const { TextInput, Checkbox, TextArea } = require("../../components/form/inputs");
const Loading = require("../../components/loading");
const BackButton = require("../../components/back-button");
const MutationButton = require("../../components/form/mutation-button");
module.exports = function InstanceDetail({ baseUrl }) {
const { data: blockedInstances = {}, isLoading } = query.useInstanceBlocksQuery();
let [_match, { domain }] = useRoute(`${baseUrl}/:domain`);
if (domain == "view") {
// Retrieve domain from form field submission.
domain = (new URL(document.location)).searchParams.get("domain");
}
// Normalize / decode domain (it may be URL-encoded).
domain = decodeURIComponent(domain);
const existingBlock = React.useMemo(() => {
return blockedInstances[domain];
}, [blockedInstances, domain]);
if (domain == undefined) {
return <Redirect to={baseUrl} />;
}
let infoContent = null;
if (isLoading) {
infoContent = <Loading />;
} else if (existingBlock == undefined) {
infoContent = <span>No stored block yet, you can add one below:</span>;
} else {
infoContent = (
<div className="info">
<i className="fa fa-fw fa-exclamation-triangle" aria-hidden="true"></i>
<b>Editing domain blocks isn't implemented yet, <a href="https://github.com/superseriousbusiness/gotosocial/issues/1198" target="_blank" rel="noopener noreferrer">check here for progress</a></b>
</div>
);
}
return (
<div>
<h1 className="text-cutoff"><BackButton to={baseUrl} /> Federation settings for: <span title={domain}>{domain}</span></h1>
{infoContent}
<DomainBlockForm defaultDomain={domain} block={existingBlock} baseUrl={baseUrl} />
</div>
);
};
function DomainBlockForm({ defaultDomain, block = {}, baseUrl }) {
const isExistingBlock = block.domain != undefined;
const disabledForm = isExistingBlock
? {
disabled: true,
title: "Domain suspensions currently cannot be edited."
}
: {};
const form = {
domain: useTextInput("domain", { source: block, defaultValue: defaultDomain }),
obfuscate: useBoolInput("obfuscate", { source: block }),
commentPrivate: useTextInput("private_comment", { source: block }),
commentPublic: useTextInput("public_comment", { source: block })
};
const [submitForm, addResult] = useFormSubmit(form, query.useAddInstanceBlockMutation(), { changedOnly: false });
const [removeBlock, removeResult] = query.useRemoveInstanceBlockMutation({ fixedCacheKey: block.id });
const [location, setLocation] = useLocation();
function verifyUrlThenSubmit(e) {
// Adding a new block happens on /settings/admin/federation/domain.com
// but if domain input changes, that doesn't match anymore and causes issues later on
// so, before submitting the form, silently change url, then submit
let correctUrl = `${baseUrl}/${form.domain.value}`;
if (location != correctUrl) {
setLocation(correctUrl);
}
return submitForm(e);
}
return (
<form onSubmit={verifyUrlThenSubmit}>
<TextInput
field={form.domain}
label="Domain"
placeholder="example.com"
{...disabledForm}
/>
<Checkbox
field={form.obfuscate}
label="Obfuscate domain in public lists"
{...disabledForm}
/>
<TextArea
field={form.commentPrivate}
label="Private comment"
rows={3}
{...disabledForm}
/>
<TextArea
field={form.commentPublic}
label="Public comment"
rows={3}
{...disabledForm}
/>
<div className="action-buttons row">
<MutationButton
label="Suspend"
result={addResult}
showError={false}
{...disabledForm}
/>
{
isExistingBlock &&
<MutationButton
type="button"
onClick={() => removeBlock(block.id)}
label="Remove"
result={removeResult}
className="button danger"
showError={false}
/>
}
</div>
{addResult.error && <Error error={addResult.error} />}
{removeResult.error && <Error error={removeResult.error} />}
</form>
);
}

View File

@ -1,75 +0,0 @@
/*
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/>.
*/
const React = require("react");
const { Switch, Route, Redirect, useLocation } = require("wouter");
const query = require("../../../lib/query");
const {
useTextInput,
} = require("../../../lib/form");
const useFormSubmit = require("../../../lib/form/submit");
const ProcessImport = require("./process");
const ImportExportForm = require("./form");
module.exports = function ImportExport({ baseUrl }) {
const form = {
domains: useTextInput("domains"),
exportType: useTextInput("exportType", { defaultValue: "plain", dontReset: true })
};
const [submitParse, parseResult] = useFormSubmit(form, query.useProcessDomainListMutation(), { changedOnly: false });
const [_location, setLocation] = useLocation();
return (
<Switch>
<Route path={`${baseUrl}/process`}>
{parseResult.isSuccess ? (
<>
<h1>
<span className="button" onClick={() => {
parseResult.reset();
setLocation(baseUrl);
}}>
&lt; back
</span> Confirm import:
</h1>
<ProcessImport
list={parseResult.data}
/>
</>
) : <Redirect to={baseUrl} />}
</Route>
<Route>
{!parseResult.isSuccess ? (
<ImportExportForm
form={form}
submitParse={submitParse}
parseResult={parseResult}
/>
) : <Redirect to={`${baseUrl}/process`} />}
</Route>
</Switch>
);
};

View File

@ -1,101 +0,0 @@
/*
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/>.
*/
const React = require("react");
const { Link, useLocation } = require("wouter");
const { matchSorter } = require("match-sorter");
const { useTextInput } = require("../../lib/form");
const { TextInput } = require("../../components/form/inputs");
const query = require("../../lib/query");
const Loading = require("../../components/loading");
module.exports = function InstanceOverview({ baseUrl }) {
const { data: blockedInstances = [], isLoading } = query.useInstanceBlocksQuery();
const [_location, setLocation] = useLocation();
const filterField = useTextInput("filter");
const filter = filterField.value;
const blockedInstancesList = React.useMemo(() => {
return Object.values(blockedInstances);
}, [blockedInstances]);
const filteredInstances = React.useMemo(() => {
return matchSorter(blockedInstancesList, filter, { keys: ["domain"] });
}, [blockedInstancesList, filter]);
let filtered = blockedInstancesList.length - filteredInstances.length;
function filterFormSubmit(e) {
e.preventDefault();
setLocation(`${baseUrl}/${filter}`);
}
if (isLoading) {
return <Loading />;
}
return (
<>
<h1>Federation</h1>
<div className="instance-list">
<h2>Suspended instances</h2>
<p>
Suspending a domain blocks all current and future accounts on that instance. Stored content will be removed,
and no more data is sent to the remote server.<br />
This extends to all subdomains as well, so blocking 'example.com' also includes 'social.example.com'.
</p>
<form className="filter" role="search" onSubmit={filterFormSubmit}>
<TextInput field={filterField} placeholder="example.com" label="Search or add domain suspension" />
<Link to={`${baseUrl}/${filter}`}><a className="button">Suspend</a></Link>
</form>
<div>
<span>
{blockedInstancesList.length} blocked instance{blockedInstancesList.length != 1 ? "s" : ""} {filtered > 0 && `(${filtered} filtered by search)`}
</span>
<div className="list">
<div className="entries scrolling">
{filteredInstances.map((entry) => {
return (
<Link key={entry.domain} to={`${baseUrl}/${entry.domain}`}>
<a className="entry nounderline">
<span id="domain">
{entry.domain}
</span>
<span id="date">
{new Date(entry.created_at).toLocaleString()}
</span>
</a>
</Link>
);
})}
</div>
</div>
</div>
</div>
<Link to={`${baseUrl}/import-export`}><a>Or use the bulk import/export interface</a></Link>
</>
);
};

View File

@ -20,19 +20,21 @@
const React = require("react");
const { useRoute, Redirect } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data");
const FormWithData = require("../../lib/form/form-with-data").default;
const BackButton = require("../../components/back-button");
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit");
const useFormSubmit = require("../../lib/form/submit").default;
const { TextArea } = require("../../components/form/inputs");
const MutationButton = require("../../components/form/mutation-button");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const {
useGetReportQuery,
useResolveReportMutation,
} = require("../../lib/query/admin/reports");
module.exports = function ReportDetail({ }) {
const baseUrl = useBaseUrl();
@ -46,7 +48,7 @@ module.exports = function ReportDetail({ }) {
<BackButton to={baseUrl} /> Report Details
</h1>
<FormWithData
dataQuery={query.useGetReportQuery}
dataQuery={useGetReportQuery}
queryArg={params.reportId}
DataForm={ReportDetailForm}
/>
@ -115,7 +117,7 @@ function ReportActionForm({ report }) {
comment: useTextInput("action_taken_comment")
};
const [submit, result] = useFormSubmit(form, query.useResolveReportMutation(), { changedOnly: false });
const [submit, result] = useFormSubmit(form, useResolveReportMutation(), { changedOnly: false });
return (
<form onSubmit={submit} className="info-block">

View File

@ -20,13 +20,12 @@
const React = require("react");
const { Link, Switch, Route } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data");
const FormWithData = require("../../lib/form/form-with-data").default;
const ReportDetail = require("./detail");
const Username = require("./username");
const { useBaseUrl } = require("../../lib/navigation/util");
const { useListReportsQuery } = require("../../lib/query/admin/reports");
module.exports = function Reports({ baseUrl }) {
return (
@ -51,7 +50,7 @@ function ReportOverview({ }) {
</p>
</div>
<FormWithData
dataQuery={query.useListReportsQuery}
dataQuery={useListReportsQuery}
DataForm={ReportsList}
/>
</>

View File

@ -19,14 +19,12 @@
const React = require("react");
const query = require("../../lib/query");
const {
useTextInput,
useFileInput
} = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit");
const useFormSubmit = require("../../lib/form/submit").default;
const {
TextInput,
@ -34,13 +32,16 @@ const {
FileInput
} = require("../../components/form/inputs");
const FormWithData = require("../../lib/form/form-with-data");
const FormWithData = require("../../lib/form/form-with-data").default;
const MutationButton = require("../../components/form/mutation-button");
const { useInstanceV1Query } = require("../../lib/query");
const { useUpdateInstanceMutation } = require("../../lib/query/admin");
module.exports = function AdminSettings() {
return (
<FormWithData
dataQuery={query.useInstanceQuery}
dataQuery={useInstanceV1Query}
DataForm={AdminSettingsForm}
/>
);
@ -61,7 +62,7 @@ function AdminSettingsForm({ data: instance }) {
terms: useTextInput("terms", { source: instance })
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateInstanceMutation());
const [submitForm, result] = useFormSubmit(form, useUpdateInstanceMutation());
return (
<form onSubmit={submitForm}>

View File

@ -21,11 +21,11 @@ const React = require("react");
const { Switch, Route, Link, Redirect, useRoute } = require("wouter");
const query = require("../../lib/query");
const FormWithData = require("../../lib/form/form-with-data");
const FormWithData = require("../../lib/form/form-with-data").default;
const { useBaseUrl } = require("../../lib/navigation/util");
const { useValue, useTextInput } = require("../../lib/form");
const useFormSubmit = require("../../lib/form/submit");
const useFormSubmit = require("../../lib/form/submit").default;
const { TextArea } = require("../../components/form/inputs");
const MutationButton = require("../../components/form/mutation-button");

View File

@ -25,6 +25,7 @@ import React from "react";
import Login from "./login";
import Loading from "../loading";
import { Error } from "../error";
import { NoArg } from "../../lib/types/query";
export function Authorization({ App }) {
const { loginState, expectingRedirect } = store.getState().oauth;
@ -35,15 +36,15 @@ export function Authorization({ App }) {
isSuccess,
data: account,
error,
} = useVerifyCredentialsQuery(null, { skip: skip });
} = useVerifyCredentialsQuery(NoArg, { skip: skip });
let showLogin = true;
let content = null;
let content: React.JSX.Element | null = null;
if (isLoading) {
showLogin = false;
let loadingInfo;
let loadingInfo = "";
if (loginState == "callback") {
loadingInfo = "Processing OAUTH callback.";
} else if (loginState == "login") {

View File

@ -22,26 +22,21 @@ import React from "react";
import { useAuthorizeFlowMutation } from "../../lib/query/oauth";
import { useTextInput, useValue } from "../../lib/form";
import useFormSubmit from "../../lib/form/submit";
import { TextInput } from "../form/inputs";
import MutationButton from "../form/mutation-button";
import Loading from "../loading";
import { TextInput } from "../form/inputs";
export default function Login({ }) {
const form = {
instance: useTextInput("instance", {
defaultValue: window.location.origin
}),
scopes: useValue("scopes", "user admin")
scopes: useValue("scopes", "user admin"),
};
const [formSubmit, result] = useFormSubmit(
form,
useAuthorizeFlowMutation(),
{
changedOnly: false,
onFinish: undefined,
}
);
const [formSubmit, result] = useFormSubmit(form, useAuthorizeFlowMutation(), {
changedOnly: false,
});
if (result.isLoading) {
return (

View File

@ -17,21 +17,31 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
module.exports = function CheckList({ field, header = "All", EntryComponent, getExtraProps }) {
import { memo, useDeferredValue, useCallback, useMemo } from "react";
import { Checkable, ChecklistInputHook } from "../lib/form/types";
interface CheckListProps {
field: ChecklistInputHook;
header: string | React.JSX.Element;
EntryComponent: React.FunctionComponent;
getExtraProps;
}
export default function CheckList({ field, header = "All", EntryComponent, getExtraProps }: CheckListProps) {
return (
<div className="checkbox-list list">
<CheckListHeader toggleAll={field.toggleAll}> {header}</CheckListHeader>
<CheckListEntries
entries={field.value}
entries={field.value ?? {}}
updateValue={field.onChange}
EntryComponent={EntryComponent}
getExtraProps={getExtraProps}
/>
</div>
);
};
}
function CheckListHeader({ toggleAll, children }) {
return (
@ -45,9 +55,16 @@ function CheckListHeader({ toggleAll, children }) {
);
}
const CheckListEntries = React.memo(
function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }) {
const deferredEntries = React.useDeferredValue(entries);
interface CheckListEntriesProps {
entries: { [_: string]: Checkable },
updateValue,
EntryComponent,
getExtraProps,
}
const CheckListEntries = memo(
function CheckListEntries({ entries, updateValue, EntryComponent, getExtraProps }: CheckListEntriesProps) {
const deferredEntries = useDeferredValue(entries);
return Object.values(deferredEntries).map((entry) => (
<CheckListEntry
@ -61,19 +78,26 @@ const CheckListEntries = React.memo(
}
);
interface CheckListEntryProps {
entry: Checkable,
updateValue,
getExtraProps,
EntryComponent,
}
/*
React.memo is a performance optimization that only re-renders a CheckListEntry
when it's props actually change, instead of every time anything
in the list (CheckListEntries) updates
*/
const CheckListEntry = React.memo(
function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }) {
const onChange = React.useCallback(
const CheckListEntry = memo(
function CheckListEntry({ entry, updateValue, getExtraProps, EntryComponent }: CheckListEntryProps) {
const onChange = useCallback(
(value) => updateValue(entry.key, value),
[updateValue, entry.key]
);
const extraProps = React.useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
const extraProps = useMemo(() => getExtraProps?.(entry), [getExtraProps, entry]);
return (
<label className="entry">

View File

@ -17,9 +17,28 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React from "react";
function TextInput({ label, field, ...inputProps }) {
import type {
ReactNode,
RefObject,
} from "react";
import type {
FileFormInputHook,
RadioFormInputHook,
TextFormInputHook,
} from "../../lib/form/types";
export interface TextInputProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label?: string;
field: TextFormInputHook;
}
export function TextInput({label, field, ...props}: TextInputProps) {
const { onChange, value, ref } = field;
return (
@ -27,16 +46,25 @@ function TextInput({ label, field, ...inputProps }) {
<label>
{label}
<input
type="text"
{...{ onChange, value, ref }}
{...inputProps}
onChange={onChange}
value={value}
ref={ref as RefObject<HTMLInputElement>}
{...props}
/>
</label>
</div>
);
}
function TextArea({ label, field, ...inputProps }) {
export interface TextAreaProps extends React.DetailedHTMLProps<
React.TextareaHTMLAttributes<HTMLTextAreaElement>,
HTMLTextAreaElement
> {
label?: string;
field: TextFormInputHook;
}
export function TextArea({label, field, ...props}: TextAreaProps) {
const { onChange, value, ref } = field;
return (
@ -44,16 +72,25 @@ function TextArea({ label, field, ...inputProps }) {
<label>
{label}
<textarea
type="text"
{...{ onChange, value, ref }}
{...inputProps}
onChange={onChange}
value={value}
ref={ref as RefObject<HTMLTextAreaElement>}
{...props}
/>
</label>
</div>
);
}
function FileInput({ label, field, ...inputProps }) {
export interface FileInputProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label?: string;
field: FileFormInputHook;
}
export function FileInput({ label, field, ...props }: FileInputProps) {
const { onChange, ref, infoComponent } = field;
return (
@ -66,15 +103,16 @@ function FileInput({ label, field, ...inputProps }) {
<input
type="file"
className="hidden"
{...{ onChange, ref }}
{...inputProps}
onChange={onChange}
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
{...props}
/>
</label>
</div>
);
}
function Checkbox({ label, field, ...inputProps }) {
export function Checkbox({ label, field, ...inputProps }) {
const { onChange, value } = field;
return (
@ -91,16 +129,29 @@ function Checkbox({ label, field, ...inputProps }) {
);
}
function Select({ label, field, options, children, ...inputProps }) {
export interface SelectProps extends React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> {
label?: string;
field: TextFormInputHook;
children?: ReactNode;
options: React.JSX.Element;
}
export function Select({ label, field, children, options, ...props }: SelectProps) {
const { onChange, value, ref } = field;
return (
<div className="form-field select">
<label>
{label} {children}
{label}
{children}
<select
{...{ onChange, value, ref }}
{...inputProps}
onChange={onChange}
value={value}
ref={ref as RefObject<HTMLSelectElement>}
{...props}
>
{options}
</select>
@ -109,7 +160,15 @@ function Select({ label, field, options, children, ...inputProps }) {
);
}
function RadioGroup({ field, label, ...inputProps }) {
export interface RadioGroupProps extends React.DetailedHTMLProps<
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label?: string;
field: RadioFormInputHook;
}
export function RadioGroup({ label, field, ...props }: RadioGroupProps) {
return (
<div className="form-field radio">
{Object.entries(field.options).map(([value, radioLabel]) => (
@ -120,7 +179,7 @@ function RadioGroup({ field, label, ...inputProps }) {
value={value}
checked={field.value == value}
onChange={field.onChange}
{...inputProps}
{...props}
/>
{radioLabel}
</label>
@ -129,12 +188,3 @@ function RadioGroup({ field, label, ...inputProps }) {
</div>
);
}
module.exports = {
TextInput,
TextArea,
FileInput,
Checkbox,
Select,
RadioGroup
};

View File

@ -18,15 +18,17 @@
*/
const React = require("react");
const query = require("../lib/query");
const Loading = require("./loading");
const {
useVerifyCredentialsQuery,
useLogoutMutation,
} = require("../lib/query/oauth");
const { useInstanceV1Query } = require("../lib/query");
module.exports = function UserLogoutCard() {
const { data: profile, isLoading } = query.useVerifyCredentialsQuery();
const { data: instance } = query.useInstanceQuery();
const [logoutQuery] = query.useLogoutMutation();
const { data: profile, isLoading } = useVerifyCredentialsQuery();
const { data: instance } = useInstanceV1Query();
const [logoutQuery] = useLogoutMutation();
if (isLoading) {
return <Loading />;

View File

@ -30,6 +30,9 @@ const Loading = require("./components/loading");
const UserLogoutCard = require("./components/user-logout-card");
const { RoleContext } = require("./lib/navigation/util");
const DomainPerms = require("./admin/domain-permissions").default;
const DomainPermsImportExport = require("./admin/domain-permissions/import-export").default;
require("./style.css");
const { Sidebar, ViewRouter } = createNavigation("/settings", [
@ -43,10 +46,11 @@ const { Sidebar, ViewRouter } = createNavigation("/settings", [
}, [
Item("Reports", { icon: "fa-flag", wildcard: true }, require("./admin/reports")),
Item("Accounts", { icon: "fa-users", wildcard: true }, require("./admin/accounts")),
Menu("Federation", { icon: "fa-hubzilla" }, [
Item("Federation", { icon: "fa-hubzilla", url: "", wildcard: true }, require("./admin/federation")),
Item("Import/Export", { icon: "fa-floppy-o", wildcard: true }, require("./admin/federation/import-export")),
])
Menu("Domain Permissions", { icon: "fa-hubzilla" }, [
Item("Blocks", { icon: "fa-close", url: "block", wildcard: true }, DomainPerms),
Item("Allows", { icon: "fa-check", url: "allow", wildcard: true }, DomainPerms),
Item("Import/Export", { icon: "fa-floppy-o", url: "import-export", wildcard: true }, DomainPermsImportExport),
]),
]),
Menu("Administration", {
url: "admin",

View File

@ -17,11 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import { useState } from "react";
import type {
BoolFormInputHook,
CreateHookNames,
HookOpts,
} from "./types";
const _default = false;
module.exports = function useBoolInput({ name, Name }, { initialValue = _default }) {
const [value, setValue] = React.useState(initialValue);
export default function useBoolInput(
{ name, Name }: CreateHookNames,
{ initialValue = _default }: HookOpts<boolean>
): BoolFormInputHook {
const [value, setValue] = useState(initialValue);
function onChange(e) {
setValue(e.target.checked);
@ -41,6 +49,7 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
}
], {
name,
Name: "",
onChange,
reset,
value,
@ -48,4 +57,4 @@ module.exports = function useBoolInput({ name, Name }, { initialValue = _default
hasChanged: () => value != initialValue,
_default
});
};
}

View File

@ -17,37 +17,58 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const syncpipe = require("syncpipe");
const { createSlice } = require("@reduxjs/toolkit");
const { enableMapSet } = require("immer");
import {
useReducer,
useRef,
useEffect,
useCallback,
useMemo,
} from "react";
enableMapSet(); // for use in reducers
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import type {
Checkable,
ChecklistInputHook,
CreateHookNames,
HookOpts,
} from "./types";
// https://immerjs.github.io/immer/installation#pick-your-immer-version
import { enableMapSet } from "immer";
enableMapSet();
interface ChecklistState {
entries: { [k: string]: Checkable },
selectedEntries: Set<string>,
}
const initialState: ChecklistState = {
entries: {},
selectedEntries: new Set(),
};
const { reducer, actions } = createSlice({
name: "checklist",
initialState: {}, // not handled by slice itself
initialState, // not handled by slice itself
reducers: {
updateAll: (state, { payload: checked }) => {
const selectedEntries = new Set();
return {
entries: syncpipe(state.entries, [
(_) => Object.values(_),
(_) => _.map((entry) => {
if (checked) {
selectedEntries.add(entry.key);
}
return [entry.key, {
...entry,
checked
}];
}),
(_) => Object.fromEntries(_)
]),
selectedEntries
};
updateAll: (state, { payload: checked }: PayloadAction<boolean>) => {
const selectedEntries = new Set<string>();
const entries = Object.fromEntries(
Object.values(state.entries).map((entry) => {
if (checked) {
// Cheekily add this to selected
// entries while we're here.
selectedEntries.add(entry.key);
}
return [entry.key, { ...entry, checked } ];
})
);
return { entries, selectedEntries };
},
update: (state, { payload: { key, value } }) => {
update: (state, { payload: { key, value } }: PayloadAction<{key: string, value: Checkable}>) => {
if (value.checked !== undefined) {
if (value.checked === true) {
state.selectedEntries.add(key);
@ -61,7 +82,7 @@ const { reducer, actions } = createSlice({
...value
};
},
updateMultiple: (state, { payload }) => {
updateMultiple: (state, { payload }: PayloadAction<Array<[key: string, value: Checkable]>>) => {
payload.forEach(([key, value]) => {
if (value.checked !== undefined) {
if (value.checked === true) {
@ -80,43 +101,57 @@ const { reducer, actions } = createSlice({
}
});
function initialState({ entries, uniqueKey, initialValue }) {
const selectedEntries = new Set();
function initialHookState({
entries,
uniqueKey,
initialValue,
}: {
entries: Checkable[],
uniqueKey: string,
initialValue: boolean,
}): ChecklistState {
const selectedEntries = new Set<string>();
const mappedEntries = Object.fromEntries(
entries.map((entry) => {
const key = entry[uniqueKey];
const checked = entry.checked ?? initialValue;
if (checked) {
selectedEntries.add(key);
} else {
selectedEntries.delete(key);
}
return [ key, { ...entry, key, checked } ];
})
);
return {
entries: syncpipe(entries, [
(_) => _.map((entry) => {
let key = entry[uniqueKey];
let checked = entry.checked ?? initialValue;
if (checked) {
selectedEntries.add(key);
} else {
selectedEntries.delete(key);
}
return [
key,
{
...entry,
key,
checked
}
];
}),
(_) => Object.fromEntries(_)
]),
entries: mappedEntries,
selectedEntries
};
}
module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "key", initialValue = false }) {
const [state, dispatch] = React.useReducer(reducer, null,
() => initialState({ entries, uniqueKey, initialValue }) // initial state
const _default: { [k: string]: Checkable } = {};
export default function useCheckListInput(
/* eslint-disable no-unused-vars */
{ name, Name }: CreateHookNames,
{
entries = [],
uniqueKey = "key",
initialValue = false,
}: HookOpts<boolean>
): ChecklistInputHook {
const [state, dispatch] = useReducer(
reducer,
initialState,
(_) => initialHookState({ entries, uniqueKey, initialValue }) // initial state
);
const toggleAllRef = React.useRef(null);
const toggleAllRef = useRef<any>(null);
React.useEffect(() => {
useEffect(() => {
if (toggleAllRef.current != null) {
let some = state.selectedEntries.size > 0;
let all = false;
@ -130,22 +165,22 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state.selectedEntries]);
const reset = React.useCallback(
const reset = useCallback(
() => dispatch(actions.updateAll(initialValue)),
[initialValue]
);
const onChange = React.useCallback(
const onChange = useCallback(
(key, value) => dispatch(actions.update({ key, value })),
[]
);
const updateMultiple = React.useCallback(
const updateMultiple = useCallback(
(entries) => dispatch(actions.updateMultiple(entries)),
[]
);
return React.useMemo(() => {
return useMemo(() => {
function toggleAll(e) {
let checked = e.target.checked;
if (e.target.indeterminate) {
@ -165,7 +200,10 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
reset,
{ name }
], {
_default,
hasChanged: () => true,
name,
Name: "",
value: state.entries,
onChange,
selectedValues,
@ -178,4 +216,4 @@ module.exports = function useCheckListInput({ name }, { entries, uniqueKey = "ke
}
});
}, [state, reset, name, onChange, updateMultiple]);
};
}

View File

@ -17,13 +17,21 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import { useState } from "react";
const { useComboboxState } = require("ariakit/combobox");
import { useComboboxState } from "ariakit/combobox";
import {
ComboboxFormInputHook,
CreateHookNames,
HookOpts,
} from "./types";
const _default = "";
module.exports = function useComboBoxInput({ name, Name }, { initialValue = _default }) {
const [isNew, setIsNew] = React.useState(false);
export default function useComboBoxInput(
{ name, Name }: CreateHookNames,
{ initialValue = _default }: HookOpts<string>
): ComboboxFormInputHook {
const [isNew, setIsNew] = useState(false);
const state = useComboboxState({
defaultValue: initialValue,
@ -45,14 +53,15 @@ module.exports = function useComboBoxInput({ name, Name }, { initialValue = _def
[`set${Name}IsNew`]: setIsNew
}
], {
reset,
name,
Name: "", // Will be set by inputHook function.
state,
value: state.value,
setter: (val) => state.setValue(val),
setter: (val: string) => state.setValue(val),
hasChanged: () => state.value != initialValue,
isNew,
setIsNew,
reset,
_default
});
};
}

View File

@ -17,12 +17,19 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import { useRef, useMemo } from "react";
const getFormMutations = require("./get-form-mutations");
import getFormMutations from "./get-form-mutations";
function parseFields(entries, length) {
const fields = [];
import type {
CreateHookNames,
HookOpts,
FieldArrayInputHook,
HookedForm,
} from "./types";
function parseFields(entries: HookedForm[], length: number): HookedForm[] {
const fields: HookedForm[] = [];
for (let i = 0; i < length; i++) {
if (entries[i] != undefined) {
@ -35,23 +42,38 @@ function parseFields(entries, length) {
return fields;
}
module.exports = function useArrayInput({ name, _Name }, { initialValue, length = 0 }) {
const fields = React.useRef({});
export default function useArrayInput(
{ name }: CreateHookNames,
{
initialValue,
length = 0,
}: HookOpts,
): FieldArrayInputHook {
const _default: HookedForm[] = Array(length);
const fields = useRef<HookedForm[]>(_default);
const value = React.useMemo(() => parseFields(initialValue, length), [initialValue, length]);
const value = useMemo(
() => parseFields(initialValue, length),
[initialValue, length],
);
function hasUpdate() {
return Object.values(fields.current).some((fieldSet) => {
const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
return updatedFields.length > 0;
});
}
return {
_default,
name,
Name: "",
value,
ctx: fields.current,
maxLength: length,
hasChanged: hasUpdate,
selectedValues() {
// if any form field changed, we need to re-send everything
const hasUpdate = Object.values(fields.current).some((fieldSet) => {
const { updatedFields } = getFormMutations(fieldSet, { changedOnly: true });
return updatedFields.length > 0;
});
if (hasUpdate) {
if (hasUpdate()) {
return Object.values(fields.current).map((fieldSet) => {
return getFormMutations(fieldSet, { changedOnly: false }).mutationData;
});
@ -60,4 +82,4 @@ module.exports = function useArrayInput({ name, _Name }, { initialValue, length
}
}
};
};
}

View File

@ -17,47 +17,67 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const prettierBytes = require("prettier-bytes");
import React from "react";
module.exports = function useFileInput({ name, _Name }, {
withPreview,
maxSize,
initialInfo = "no file selected"
} = {}) {
const [file, setFile] = React.useState();
const [imageURL, setImageURL] = React.useState();
const [info, setInfo] = React.useState();
import { useState } from "react";
import prettierBytes from "prettier-bytes";
function onChange(e) {
let file = e.target.files[0];
import type {
CreateHookNames,
HookOpts,
FileFormInputHook,
} from "./types";
const _default = undefined;
export default function useFileInput(
{ name }: CreateHookNames,
{
withPreview,
maxSize,
initialInfo = "no file selected"
}: HookOpts<File>
): FileFormInputHook {
const [file, setFile] = useState<File>();
const [imageURL, setImageURL] = useState<string>();
const [info, setInfo] = useState<React.JSX.Element>();
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const files = e.target.files;
if (!files) {
setInfo(undefined);
return;
}
let file = files[0];
setFile(file);
URL.revokeObjectURL(imageURL);
if (file != undefined) {
if (withPreview) {
setImageURL(URL.createObjectURL(file));
}
let size = prettierBytes(file.size);
if (maxSize && file.size > maxSize) {
size = <span className="error-text">{size}</span>;
}
setInfo(<>
{file.name} ({size})
</>);
} else {
setInfo();
if (imageURL) {
URL.revokeObjectURL(imageURL);
}
if (withPreview) {
setImageURL(URL.createObjectURL(file));
}
let size = prettierBytes(file.size);
if (maxSize && file.size > maxSize) {
size = <span className="error-text">{size}</span>;
}
setInfo(
<>
{file.name} ({size})
</>
);
}
function reset() {
URL.revokeObjectURL(imageURL);
setImageURL();
setFile();
setInfo();
if (imageURL) {
URL.revokeObjectURL(imageURL);
}
setImageURL(undefined);
setFile(undefined);
setInfo(undefined);
}
const infoComponent = (
@ -82,9 +102,11 @@ module.exports = function useFileInput({ name, _Name }, {
onChange,
reset,
name,
Name: "", // Will be set by inputHook function.
value: file,
previewValue: imageURL,
hasChanged: () => file != undefined,
infoComponent
infoComponent,
_default,
});
};
}

View File

@ -17,14 +17,31 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Error } = require("../../components/error");
/* eslint-disable no-unused-vars */
const Loading = require("../../components/loading");
import React from "react";
// Wrap Form component inside component that fires the RTK Query call,
// so Form will only be rendered when data is available to generate form-fields for
module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formProps }) {
import { Error } from "../../components/error";
import Loading from "../../components/loading";
import { NoArg } from "../types/query";
import { FormWithDataQuery } from "./types";
export interface FormWithDataProps {
dataQuery: FormWithDataQuery,
DataForm: ({ data, ...props }) => React.JSX.Element,
queryArg?: any,
}
/**
* Wrap Form component inside component that fires the RTK Query call, so Form
* will only be rendered when data is available to generate form-fields for.
*/
export default function FormWithData({ dataQuery, DataForm, queryArg, ...props }: FormWithDataProps) {
if (!queryArg) {
queryArg = NoArg;
}
// Trigger provided query.
const { data, isLoading, isError, error } = dataQuery(queryArg);
if (isLoading) {
@ -38,6 +55,6 @@ module.exports = function FormWithData({ dataQuery, DataForm, queryArg, ...formP
<Error error={error} />
);
} else {
return <DataForm data={data} {...formProps} />;
return <DataForm data={data} {...props} />;
}
};
}

View File

@ -17,29 +17,31 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const syncpipe = require("syncpipe");
import { FormInputHook, HookedForm } from "./types";
export default function getFormMutations(
form: HookedForm,
{ changedOnly }: { changedOnly: boolean },
) {
const updatedFields: FormInputHook[] = [];
const mutationData: Array<[string, any]> = [];
Object.values(form).forEach((field) => {
if ("selectedValues" in field) {
// FieldArrayInputHook.
const selected = field.selectedValues();
if (!changedOnly || selected.length > 0) {
updatedFields.push(field);
mutationData.push([field.name, selected]);
}
} else if (!changedOnly || field.hasChanged()) {
updatedFields.push(field);
mutationData.push([field.name, field.value]);
}
});
module.exports = function getFormMutations(form, { changedOnly }) {
let updatedFields = [];
return {
updatedFields,
mutationData: syncpipe(form, [
(_) => Object.values(_),
(_) => _.map((field) => {
if (field.selectedValues != undefined) {
let selected = field.selectedValues();
if (!changedOnly || selected.length > 0) {
updatedFields.push(field);
return [field.name, selected];
}
} else if (!changedOnly || field.hasChanged()) {
updatedFields.push(field);
return [field.name, field.value];
}
return null;
}),
(_) => _.filter((value) => value != null),
(_) => Object.fromEntries(_)
])
mutationData: Object.fromEntries(mutationData),
};
};
}

View File

@ -1,83 +0,0 @@
/*
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/>.
*/
const React = require("react");
const getByDot = require("get-by-dot").default;
function capitalizeFirst(str) {
return str.slice(0, 1).toUpperCase + str.slice(1);
}
function selectorByKey(key) {
if (key.includes("[")) {
// get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
key = key
.replace(/\[/g, ".") // nested.deeper].key]
.replace(/\]/g, ""); // nested.deeper.key
}
return function selector(obj) {
if (obj == undefined) {
return undefined;
} else {
return getByDot(obj, key);
}
};
}
function makeHook(hookFunction) {
return function (name, opts = {}) {
// for dynamically generating attributes like 'setName'
const Name = React.useMemo(() => capitalizeFirst(name), [name]);
const selector = React.useMemo(() => selectorByKey(name), [name]);
const valueSelector = opts.valueSelector ?? selector;
opts.initialValue = React.useMemo(() => {
if (opts.source == undefined) {
return opts.defaultValue;
} else {
return valueSelector(opts.source) ?? opts.defaultValue;
}
}, [opts.source, opts.defaultValue, valueSelector]);
const hook = hookFunction({ name, Name }, opts);
return Object.assign(hook, {
name, Name,
});
};
}
module.exports = {
useTextInput: makeHook(require("./text")),
useFileInput: makeHook(require("./file")),
useBoolInput: makeHook(require("./bool")),
useRadioInput: makeHook(require("./radio")),
useComboBoxInput: makeHook(require("./combo-box")),
useCheckListInput: makeHook(require("./check-list")),
useFieldArrayInput: makeHook(require("./field-array")),
useValue: function (name, value) {
return {
name,
value,
hasChanged: () => true // always included
};
}
};

View File

@ -0,0 +1,114 @@
/*
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 { useMemo } from "react";
import getByDot from "get-by-dot";
import text from "./text";
import file from "./file";
import bool from "./bool";
import radio from "./radio";
import combobox from "./combo-box";
import checklist from "./check-list";
import fieldarray from "./field-array";
import type {
CreateHook,
FormInputHook,
HookOpts,
TextFormInputHook,
RadioFormInputHook,
FileFormInputHook,
BoolFormInputHook,
ComboboxFormInputHook,
FieldArrayInputHook,
ChecklistInputHook,
} from "./types";
function capitalizeFirst(str: string) {
return str.slice(0, 1).toUpperCase + str.slice(1);
}
function selectorByKey(key: string) {
if (key.includes("[")) {
// get-by-dot does not support 'nested[deeper][key]' notation, convert to 'nested.deeper.key'
key = key
.replace(/\[/g, ".") // nested.deeper].key]
.replace(/\]/g, ""); // nested.deeper.key
}
return function selector(obj) {
if (obj == undefined) {
return undefined;
} else {
return getByDot(obj, key);
}
};
}
/**
* Memoized hook generator function. Take a createHook
* function and use it to return a new FormInputHook function.
*
* @param createHook
* @returns
*/
function inputHook(createHook: CreateHook): (_name: string, _opts: HookOpts) => FormInputHook {
return (name: string, opts?: HookOpts): FormInputHook => {
// for dynamically generating attributes like 'setName'
const Name = useMemo(() => capitalizeFirst(name), [name]);
const selector = useMemo(() => selectorByKey(name), [name]);
const valueSelector = opts?.valueSelector?? selector;
if (opts) {
opts.initialValue = useMemo(() => {
if (opts.source == undefined) {
return opts.defaultValue;
} else {
return valueSelector(opts.source) ?? opts.defaultValue;
}
}, [opts.source, opts.defaultValue, valueSelector]);
}
const hook = createHook({ name, Name }, opts ?? {});
return Object.assign(hook, { name, Name });
};
}
/**
* Simplest form hook type in town.
*/
function value<T>(name: string, initialValue: T) {
return {
_default: initialValue,
name,
Name: "",
value: initialValue,
hasChanged: () => true, // always included
};
}
export const useTextInput = inputHook(text) as (_name: string, _opts?: HookOpts<string>) => TextFormInputHook;
export const useFileInput = inputHook(file) as (_name: string, _opts?: HookOpts<File>) => FileFormInputHook;
export const useBoolInput = inputHook(bool) as (_name: string, _opts?: HookOpts<boolean>) => BoolFormInputHook;
export const useRadioInput = inputHook(radio) as (_name: string, _opts?: HookOpts<string>) => RadioFormInputHook;
export const useComboBoxInput = inputHook(combobox) as (_name: string, _opts?: HookOpts<string>) => ComboboxFormInputHook;
export const useCheckListInput = inputHook(checklist) as (_name: string, _opts?: HookOpts<boolean>) => ChecklistInputHook;
export const useFieldArrayInput = inputHook(fieldarray) as (_name: string, _opts?: HookOpts<string>) => FieldArrayInputHook;
export const useValue = value as <T>(_name: string, _initialValue: T) => FormInputHook<T>;

View File

@ -17,11 +17,18 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import { useState } from "react";
import { CreateHookNames, HookOpts, RadioFormInputHook } from "./types";
const _default = "";
module.exports = function useRadioInput({ name, Name }, { initialValue = _default, options }) {
const [value, setValue] = React.useState(initialValue);
export default function useRadioInput(
{ name, Name }: CreateHookNames,
{
initialValue = _default,
options = {},
}: HookOpts<string>
): RadioFormInputHook {
const [value, setValue] = useState(initialValue);
function onChange(e) {
setValue(e.target.value);
@ -40,13 +47,14 @@ module.exports = function useRadioInput({ name, Name }, { initialValue = _defaul
[`set${Name}`]: setValue
}
], {
name,
onChange,
reset,
name,
Name: "",
value,
setter: setValue,
options,
hasChanged: () => value != initialValue,
_default
});
};
}

View File

@ -1,67 +0,0 @@
/*
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/>.
*/
const Promise = require("bluebird");
const React = require("react");
const getFormMutations = require("./get-form-mutations");
module.exports = function useFormSubmit(form, mutationQuery, { changedOnly = true, onFinish } = {}) {
if (!Array.isArray(mutationQuery)) {
throw new ("useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?");
}
const [runMutation, result] = mutationQuery;
const usedAction = React.useRef(null);
return [
function submitForm(e) {
let action;
if (e?.preventDefault) {
e.preventDefault();
action = e.nativeEvent.submitter.name;
} else {
action = e;
}
if (action == "") {
action = undefined;
}
usedAction.current = action;
// transform the field definitions into an object with just their values
const { mutationData, updatedFields } = getFormMutations(form, { changedOnly });
if (updatedFields.length == 0) {
return;
}
mutationData.action = action;
return Promise.try(() => {
return runMutation(mutationData);
}).then((res) => {
if (onFinish) {
return onFinish(res);
}
});
},
{
...result,
action: usedAction.current
}
];
};

View File

@ -0,0 +1,140 @@
/*
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 getFormMutations from "./get-form-mutations";
import { useRef } from "react";
import type {
MutationTrigger,
UseMutationStateResult,
} from "@reduxjs/toolkit/dist/query/react/buildHooks";
import type {
FormSubmitEvent,
FormSubmitFunction,
FormSubmitResult,
HookedForm,
} from "./types";
interface UseFormSubmitOptions {
changedOnly: boolean;
onFinish?: ((_res: any) => void);
}
/**
* Parse changed values from the hooked form into a request
* body, and submit it using the given mutation trigger.
*
* This function basically wraps RTK Query's submit methods to
* work with our hooked form interface.
*
* An `onFinish` callback function can be provided, which will
* be executed on a **successful** run of the given MutationTrigger,
* with the mutation result passed into it.
*
* If `changedOnly` is false, then **all** fields of the given HookedForm
* will be submitted to the mutation endpoint, not just changed ones.
*
* The returned function and result can be triggered and read
* from just like an RTK Query mutation hook result would be.
*
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
*/
export default function useFormSubmit(
form: HookedForm,
mutationQuery: readonly [MutationTrigger<any>, UseMutationStateResult<any, any>],
opts: UseFormSubmitOptions = { changedOnly: true }
): [ FormSubmitFunction, FormSubmitResult ] {
if (!Array.isArray(mutationQuery)) {
throw "useFormSubmit: mutationQuery was not an Array. Is a valid useMutation RTK Query provided?";
}
const { changedOnly, onFinish } = opts;
const [runMutation, mutationResult] = mutationQuery;
const usedAction = useRef<FormSubmitEvent>(undefined);
const submitForm = async(e: FormSubmitEvent) => {
let action: FormSubmitEvent;
if (typeof e === "string") {
if (e !== "") {
// String action name was provided.
action = e;
} else {
// Empty string action name was provided.
action = undefined;
}
} else if (e) {
// Submit event action was provided.
e.preventDefault();
if (e.nativeEvent.submitter) {
// We want the name of the element that was invoked to submit this form,
// which will be something that extends HTMLElement, though we don't know
// what at this point.
//
// See: https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent/submitter
action = (e.nativeEvent.submitter as Object as { name: string }).name;
} else {
// No submitter defined. Fall back
// to just use the FormSubmitEvent.
action = e;
}
} else {
// Void or null or something
// else was provided.
action = undefined;
}
usedAction.current = action;
// Transform the hooked form into an object.
const {
mutationData,
updatedFields,
} = getFormMutations(form, { changedOnly });
// If there were no updated fields according to
// the form parsing then there's nothing for us
// to do, since remote and desired state match.
if (updatedFields.length == 0) {
return;
}
mutationData.action = action;
try {
const res = await runMutation(mutationData);
if (onFinish) {
onFinish(res);
}
} catch (e) {
// eslint-disable-next-line no-console
console.error(`caught error running mutation: ${e}`);
}
};
return [
submitForm,
{
...mutationResult,
action: usedAction.current
}
];
}

View File

@ -17,26 +17,40 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
import React, {
useState,
useRef,
useTransition,
useEffect,
} from "react";
import type {
CreateHookNames,
HookOpts,
TextFormInputHook,
} from "./types";
const _default = "";
module.exports = function useTextInput({ name, Name }, {
initialValue = _default,
dontReset = false,
validator,
showValidation = true,
initValidation
} = {}) {
const [text, setText] = React.useState(initialValue);
const textRef = React.useRef(null);
export default function useTextInput(
{ name, Name }: CreateHookNames,
{
initialValue = _default,
dontReset = false,
validator,
showValidation = true,
initValidation
}: HookOpts<string>
): TextFormInputHook {
const [text, setText] = useState(initialValue);
const textRef = useRef<HTMLInputElement>(null);
const [validation, setValidation] = React.useState(initValidation ?? "");
const [_isValidating, startValidation] = React.useTransition();
let valid = validation == "";
const [validation, setValidation] = useState(initValidation ?? "");
const [_isValidating, startValidation] = useTransition();
const valid = validation == "";
function onChange(e) {
let input = e.target.value;
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
const input = e.target.value;
setText(input);
if (validator) {
@ -52,7 +66,7 @@ module.exports = function useTextInput({ name, Name }, {
}
}
React.useEffect(() => {
useEffect(() => {
if (validator && textRef.current) {
if (showValidation) {
textRef.current.setCustomValidity(validation);
@ -76,12 +90,13 @@ module.exports = function useTextInput({ name, Name }, {
onChange,
reset,
name,
Name: "", // Will be set by inputHook function.
value: text,
ref: textRef,
setter: setText,
valid,
validate: () => setValidation(validator(text)),
validate: () => setValidation(validator ? validator(text): ""),
hasChanged: () => text != initialValue,
_default
});
};
}

View File

@ -0,0 +1,264 @@
/*
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/>.
*/
/* eslint-disable no-unused-vars */
import { ComboboxState } from "ariakit";
import React from "react";
import {
ChangeEventHandler,
Dispatch,
RefObject,
SetStateAction,
SyntheticEvent,
} from "react";
export interface CreateHookNames {
name: string;
Name: string;
}
export interface HookOpts<T = any> {
initialValue?: T,
defaultValue?: T,
dontReset?: boolean,
validator?,
showValidation?: boolean,
initValidation?: string,
length?: number;
options?: { [_: string]: string },
withPreview?: boolean,
maxSize?,
initialInfo?: string;
valueSelector?: Function,
source?,
// checklist input types
entries?: any[];
uniqueKey?: string;
}
export type CreateHook = (
name: CreateHookNames,
opts: HookOpts,
) => FormInputHook;
export interface FormInputHook<T = any> {
/**
* Name of this FormInputHook, as provided
* in the UseFormInputHook options.
*/
name: string;
/**
* `name` with first letter capitalized.
*/
Name: string;
/**
* Current value of this FormInputHook.
*/
value?: T;
/**
* Default value of this FormInputHook.
*/
_default: T;
/**
* Return true if the values of this hook is considered
* to have been changed from the default / initial value.
*/
hasChanged: () => boolean;
}
interface _withReset {
reset: () => void;
}
interface _withOnChange {
onChange: ChangeEventHandler;
}
interface _withSetter<T> {
setter: Dispatch<SetStateAction<T>>;
}
interface _withValidate {
valid: boolean;
validate: () => void;
}
interface _withRef {
ref: RefObject<HTMLElement>;
}
interface _withFile {
previewValue?: string;
infoComponent: React.JSX.Element;
}
interface _withComboboxState {
state: ComboboxState;
}
interface _withNew {
isNew: boolean;
setIsNew: Dispatch<SetStateAction<boolean>>;
}
interface _withSelectedValues {
selectedValues: () => {
[_: string]: any;
}[]
}
interface _withCtx {
ctx
}
interface _withMaxLength {
maxLength: number;
}
interface _withOptions {
options: { [_: string]: string };
}
interface _withToggleAll {
toggleAll: _withRef & _withOnChange
}
interface _withSomeSelected {
someSelected: boolean;
}
interface _withUpdateMultiple {
updateMultiple: (_entries: any) => void;
}
export interface TextFormInputHook extends FormInputHook<string>,
_withSetter<string>,
_withOnChange,
_withReset,
_withValidate,
_withRef {}
export interface RadioFormInputHook extends FormInputHook<string>,
_withSetter<string>,
_withOnChange,
_withOptions,
_withReset {}
export interface FileFormInputHook extends FormInputHook<File | undefined>,
_withOnChange,
_withReset,
Partial<_withRef>,
_withFile {}
export interface BoolFormInputHook extends FormInputHook<boolean>,
_withSetter<boolean>,
_withOnChange,
_withReset {}
export interface ComboboxFormInputHook extends FormInputHook<string>,
_withSetter<string>,
_withComboboxState,
_withNew,
_withReset {}
export interface FieldArrayInputHook extends FormInputHook<HookedForm[]>,
_withSelectedValues,
_withMaxLength,
_withCtx {}
export interface Checkable {
key: string;
checked?: boolean;
}
export interface ChecklistInputHook<T = Checkable> extends FormInputHook<{[k: string]: T}>,
_withReset,
_withToggleAll,
_withSelectedValues,
_withSomeSelected,
_withUpdateMultiple {
// Uses its own funky onChange handler.
onChange: (key: any, value: any) => void
}
export type AnyFormInputHook =
FormInputHook |
TextFormInputHook |
RadioFormInputHook |
FileFormInputHook |
BoolFormInputHook |
ComboboxFormInputHook |
FieldArrayInputHook |
ChecklistInputHook;
export interface HookedForm {
[_: string]: AnyFormInputHook
}
/**
* Parameters for FormSubmitFunction.
*/
export type FormSubmitEvent = (string | SyntheticEvent<HTMLFormElement, Partial<SubmitEvent>> | undefined | void)
/**
* Shadows "trigger" function for useMutation, but can also
* be passed to onSubmit property of forms as a handler.
*
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#mutation-hook-behavior
*/
export type FormSubmitFunction = ((_e: FormSubmitEvent) => void)
/**
* Shadows redux mutation hook return values.
*
* See: https://redux-toolkit.js.org/rtk-query/usage/mutations#frequently-used-mutation-hook-return-values
*/
export interface FormSubmitResult {
/**
* Action used to submit the form, if any.
*/
action: FormSubmitEvent;
data: any;
error: any;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
reset: () => void;
}
/**
* Shadows redux query hook return values.
*
* See: https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values
*/
export type FormWithDataQuery = (_queryArg: any) => {
data?: any;
isLoading: boolean;
isError: boolean;
error?: any;
}

View File

@ -1,194 +0,0 @@
/*
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/>.
*/
const Promise = require("bluebird");
const { unwrapRes } = require("../lib");
module.exports = (build) => ({
listEmoji: build.query({
query: (params = {}) => ({
url: "/api/v1/admin/custom_emojis",
params: {
limit: 0,
...params
}
}),
providesTags: (res) =>
res
? [...res.map((emoji) => ({ type: "Emoji", id: emoji.id })), { type: "Emoji", id: "LIST" }]
: [{ type: "Emoji", id: "LIST" }]
}),
getEmoji: build.query({
query: (id) => ({
url: `/api/v1/admin/custom_emojis/${id}`
}),
providesTags: (res, error, id) => [{ type: "Emoji", id }]
}),
addEmoji: build.mutation({
query: (form) => {
return {
method: "POST",
url: `/api/v1/admin/custom_emojis`,
asForm: true,
body: form,
discardEmpty: true
};
},
invalidatesTags: (res) =>
res
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
: [{ type: "Emoji", id: "LIST" }]
}),
editEmoji: build.mutation({
query: ({ id, ...patch }) => {
return {
method: "PATCH",
url: `/api/v1/admin/custom_emojis/${id}`,
asForm: true,
body: {
type: "modify",
...patch
}
};
},
invalidatesTags: (res) =>
res
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
: [{ type: "Emoji", id: "LIST" }]
}),
deleteEmoji: build.mutation({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/custom_emojis/${id}`
}),
invalidatesTags: (res, error, id) => [{ type: "Emoji", id }]
}),
searchStatusForEmoji: build.mutation({
queryFn: (url, api, _extraOpts, baseQuery) => {
return Promise.try(() => {
return baseQuery({
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
}).then(unwrapRes);
}).then((searchRes) => {
return emojiFromSearchResult(searchRes);
}).then(({ type, domain, list }) => {
const state = api.getState();
if (domain == new URL(state.oauth.instance).host) {
throw "LOCAL_INSTANCE";
}
// search for every mentioned emoji with the admin api to get their ID
return Promise.map(list, (emoji) => {
return baseQuery({
url: `/api/v1/admin/custom_emojis`,
params: {
filter: `domain:${domain},shortcode:${emoji.shortcode}`,
limit: 1
}
}).then((unwrapRes)).then((list) => list[0]);
}, { concurrency: 5 }).then((listWithIDs) => {
return {
data: {
type,
domain,
list: listWithIDs
}
};
});
}).catch((e) => {
return { error: e };
});
}
}),
patchRemoteEmojis: build.mutation({
queryFn: ({ action, ...formData }, _api, _extraOpts, baseQuery) => {
const data = [];
const errors = [];
return Promise.each(formData.selectedEmoji, (emoji) => {
return Promise.try(() => {
let body = {
type: action
};
if (action == "copy") {
body.shortcode = emoji.shortcode;
if (formData.category.trim().length != 0) {
body.category = formData.category;
}
}
return baseQuery({
method: "PATCH",
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
asForm: true,
body: body
}).then(unwrapRes);
}).then((res) => {
data.push([emoji.id, res]);
}).catch((e) => {
let msg = e.message ?? e;
if (e.data.error) {
msg = e.data.error;
}
errors.push([emoji.shortcode, msg]);
});
}).then(() => {
if (errors.length == 0) {
return { data };
} else {
return {
error: errors
};
}
});
},
invalidatesTags: () => [{ type: "Emoji", id: "LIST" }]
})
});
function emojiFromSearchResult(searchRes) {
/* Parses the search response, prioritizing a toot result,
and returns referenced custom emoji
*/
let type;
if (searchRes.statuses.length > 0) {
type = "statuses";
} else if (searchRes.accounts.length > 0) {
type = "accounts";
} else {
throw "NONE_FOUND";
}
let data = searchRes[type][0];
return {
type,
domain: (new URL(data.url)).host, // to get WEB_DOMAIN, see https://github.com/superseriousbusiness/gotosocial/issues/1225
list: data.emojis
};
}

View File

@ -0,0 +1,307 @@
/*
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 { gtsApi } from "../../gts-api";
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { RootState } from "../../../../redux/store";
import type { CustomEmoji, EmojisFromItem, ListEmojiParams } from "../../../types/custom-emoji";
/**
* Parses the search response, prioritizing a status
* result, and returns any referenced custom emoji.
*
* Due to current API constraints, the returned emojis
* will not have their ID property set, so further
* processing is required to retrieve the IDs.
*
* @param searchRes
* @returns
*/
function emojisFromSearchResult(searchRes): EmojisFromItem {
// We don't know in advance whether a searched URL
// is the URL for a status, or the URL for an account,
// but we can derive this by looking at which search
// result field actually has entries in it (if any).
let type: "statuses" | "accounts";
if (searchRes.statuses.length > 0) {
// We had status results,
// so this was a status URL.
type = "statuses";
} else if (searchRes.accounts.length > 0) {
// We had account results,
// so this was an account URL.
type = "accounts";
} else {
// Nada, zilch, we can't do
// anything with this.
throw "NONE_FOUND";
}
// Narrow type to discard all the other
// data on the result that we don't need.
const data: {
url: string;
emojis: CustomEmoji[];
} = searchRes[type][0];
return {
type,
// Workaround to get host rather than account domain.
// See https://github.com/superseriousbusiness/gotosocial/issues/1225.
domain: (new URL(data.url)).host,
list: data.emojis,
};
}
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
listEmoji: build.query<CustomEmoji[], ListEmojiParams | void>({
query: (params = {}) => ({
url: "/api/v1/admin/custom_emojis",
params: {
limit: 0,
...params
}
}),
providesTags: (res, _error, _arg) =>
res
? [
...res.map((emoji) => ({ type: "Emoji" as const, id: emoji.id })),
{ type: "Emoji", id: "LIST" }
]
: [{ type: "Emoji", id: "LIST" }]
}),
getEmoji: build.query<CustomEmoji, string>({
query: (id) => ({
url: `/api/v1/admin/custom_emojis/${id}`
}),
providesTags: (_res, _error, id) => [{ type: "Emoji", id }]
}),
addEmoji: build.mutation<CustomEmoji, Object>({
query: (form) => {
return {
method: "POST",
url: `/api/v1/admin/custom_emojis`,
asForm: true,
body: form,
discardEmpty: true
};
},
invalidatesTags: (res) =>
res
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
: [{ type: "Emoji", id: "LIST" }]
}),
editEmoji: build.mutation<CustomEmoji, any>({
query: ({ id, ...patch }) => {
return {
method: "PATCH",
url: `/api/v1/admin/custom_emojis/${id}`,
asForm: true,
body: {
type: "modify",
...patch
}
};
},
invalidatesTags: (res) =>
res
? [{ type: "Emoji", id: "LIST" }, { type: "Emoji", id: res.id }]
: [{ type: "Emoji", id: "LIST" }]
}),
deleteEmoji: build.mutation<any, string>({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/custom_emojis/${id}`
}),
invalidatesTags: (_res, _error, id) => [{ type: "Emoji", id }]
}),
searchItemForEmoji: build.mutation<EmojisFromItem, string>({
async queryFn(url, api, _extraOpts, fetchWithBQ) {
const state = api.getState() as RootState;
const oauthState = state.oauth;
// First search for given url.
const searchRes = await fetchWithBQ({
url: `/api/v2/search?q=${encodeURIComponent(url)}&resolve=true&limit=1`
});
if (searchRes.error) {
return { error: searchRes.error as FetchBaseQueryError };
}
// Parse initial results of search.
// These emojis will not have IDs set.
const {
type,
domain,
list: withoutIDs,
} = emojisFromSearchResult(searchRes.data);
// Ensure emojis domain is not OUR domain. If it
// is, we already have the emojis by definition.
if (oauthState.instanceUrl !== undefined) {
if (domain == new URL(oauthState.instanceUrl).host) {
throw "LOCAL_INSTANCE";
}
}
// Search for each listed emoji with the admin
// api to get the version that includes an ID.
const withIDs: CustomEmoji[] = [];
const errors: FetchBaseQueryError[] = [];
withoutIDs.forEach(async(emoji) => {
// Request admin view of this emoji.
const emojiRes = await fetchWithBQ({
url: `/api/v1/admin/custom_emojis`,
params: {
filter: `domain:${domain},shortcode:${emoji.shortcode}`,
limit: 1
}
});
if (emojiRes.error) {
errors.push(emojiRes.error);
} else {
// Got it!
withIDs.push(emojiRes.data as CustomEmoji);
}
});
if (errors.length !== 0) {
return {
error: {
status: 400,
statusText: 'Bad Request',
data: {"error":`One or more errors fetching custom emojis: ${errors}`},
},
};
}
// Return our ID'd
// emojis list.
return {
data: {
type,
domain,
list: withIDs,
}
};
}
}),
patchRemoteEmojis: build.mutation({
async queryFn({ action, ...formData }, _api, _extraOpts, fetchWithBQ) {
const data: CustomEmoji[] = [];
const errors: FetchBaseQueryError[] = [];
formData.selectEmoji.forEach(async(emoji: CustomEmoji) => {
let body = {
type: action,
shortcode: "",
category: "",
};
if (action == "copy") {
body.shortcode = emoji.shortcode;
if (formData.category.trim().length != 0) {
body.category = formData.category;
}
}
const emojiRes = await fetchWithBQ({
method: "PATCH",
url: `/api/v1/admin/custom_emojis/${emoji.id}`,
asForm: true,
body: body
});
if (emojiRes.error) {
errors.push(emojiRes.error);
} else {
// Got it!
data.push(emojiRes.data as CustomEmoji);
}
});
if (errors.length !== 0) {
return {
error: {
status: 400,
statusText: 'Bad Request',
data: {"error":`One or more errors patching custom emojis: ${errors}`},
},
};
}
return { data };
},
invalidatesTags: () => [{ type: "Emoji", id: "LIST" }]
})
})
});
/**
* List all custom emojis uploaded on our local instance.
*/
const useListEmojiQuery = extended.useListEmojiQuery;
/**
* Get a single custom emoji uploaded on our local instance, by its ID.
*/
const useGetEmojiQuery = extended.useGetEmojiQuery;
/**
* Add a new custom emoji by uploading it to our local instance.
*/
const useAddEmojiMutation = extended.useAddEmojiMutation;
/**
* Edit an existing custom emoji that's already been uploaded to our local instance.
*/
const useEditEmojiMutation = extended.useEditEmojiMutation;
/**
* Delete a single custom emoji from our local instance using its id.
*/
const useDeleteEmojiMutation = extended.useDeleteEmojiMutation;
/**
* "Steal this look" function for selecting remote emoji from a status or account.
*/
const useSearchItemForEmojiMutation = extended.useSearchItemForEmojiMutation;
/**
* Update/patch a bunch of remote emojis.
*/
const usePatchRemoteEmojisMutation = extended.usePatchRemoteEmojisMutation;
export {
useListEmojiQuery,
useGetEmojiQuery,
useAddEmojiMutation,
useEditEmojiMutation,
useDeleteEmojiMutation,
useSearchItemForEmojiMutation,
usePatchRemoteEmojisMutation,
};

View File

@ -0,0 +1,155 @@
/*
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 fileDownload from "js-file-download";
import { unparse as csvUnparse } from "papaparse";
import { gtsApi } from "../../gts-api";
import { RootState } from "../../../../redux/store";
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
import { DomainPerm, ExportDomainPermsParams } from "../../../types/domain-permission";
interface _exportProcess {
transformEntry: (_entry: DomainPerm) => any;
stringify: (_list: any[]) => string;
extension: string;
mime: string;
}
/**
* Derive process functions and metadata
* from provided export request form.
*
* @param formData
* @returns
*/
function exportProcess(formData: ExportDomainPermsParams): _exportProcess {
if (formData.exportType == "json") {
return {
transformEntry: (entry) => ({
domain: entry.domain,
public_comment: entry.public_comment,
obfuscate: entry.obfuscate
}),
stringify: (list) => JSON.stringify(list),
extension: ".json",
mime: "application/json"
};
}
if (formData.exportType == "csv") {
return {
transformEntry: (entry) => [
entry.domain, // #domain
"suspend", // #severity
false, // #reject_media
false, // #reject_reports
entry.public_comment, // #public_comment
entry.obfuscate ?? false // #obfuscate
],
stringify: (list) => csvUnparse({
fields: [
"#domain",
"#severity",
"#reject_media",
"#reject_reports",
"#public_comment",
"#obfuscate",
],
data: list
}),
extension: ".csv",
mime: "text/csv"
};
}
// Fall back to plain text export.
return {
transformEntry: (entry) => entry.domain,
stringify: (list) => list.join("\n"),
extension: ".txt",
mime: "text/plain"
};
}
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
exportDomainList: build.mutation<string | null, ExportDomainPermsParams>({
async queryFn(formData, api, _extraOpts, fetchWithBQ) {
// Fetch domain perms from relevant endpoint.
// We could have used 'useDomainBlocksQuery'
// or 'useDomainAllowsQuery' for this, but
// we want the untransformed array version.
const permsRes = await fetchWithBQ({ url: `/api/v1/admin/domain_${formData.permType}s` });
if (permsRes.error) {
return { error: permsRes.error as FetchBaseQueryError };
}
// Process perms into desired export format.
const process = exportProcess(formData);
const transformed = (permsRes.data as DomainPerm[]).map(process.transformEntry);
const exportAsString = process.stringify(transformed);
if (formData.action == "export") {
// Data will just be exported
// to the domains text field.
return { data: exportAsString };
}
// File export has been requested.
// Parse filename to something like:
// `example.org-blocklist-2023-10-09.json`.
const state = api.getState() as RootState;
const instanceUrl = state.oauth.instanceUrl?? "unknown";
const domain = new URL(instanceUrl).host;
const date = new Date();
const filename = [
domain,
"blocklist",
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"),
].join("-");
fileDownload(
exportAsString,
filename + process.extension,
process.mime
);
// js-file-download handles the
// nitty gritty for us, so we can
// just return null data.
return { data: null };
}
}),
})
});
/**
* Makes a GET to `/api/v1/admin/domain_{perm_type}s`
* and exports the result in the requested format.
*
* Return type will be string if `action` is "export",
* else it will be null, since the file downloader handles
* the rest of the request then.
*/
const useExportDomainListMutation = extended.useExportDomainListMutation;
export { useExportDomainListMutation };

View File

@ -0,0 +1,56 @@
/*
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 { gtsApi } from "../../gts-api";
import type { DomainPerm, MappedDomainPerms } from "../../../types/domain-permission";
import { listToKeyedObject } from "../../transforms";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
domainBlocks: build.query<MappedDomainPerms, void>({
query: () => ({
url: `/api/v1/admin/domain_blocks`
}),
transformResponse: listToKeyedObject<DomainPerm>("domain"),
}),
domainAllows: build.query<MappedDomainPerms, void>({
query: () => ({
url: `/api/v1/admin/domain_allows`
}),
transformResponse: listToKeyedObject<DomainPerm>("domain"),
}),
}),
});
/**
* Get admin view of all explicitly blocked domains.
*/
const useDomainBlocksQuery = extended.useDomainBlocksQuery;
/**
* Get admin view of all explicitly allowed domains.
*/
const useDomainAllowsQuery = extended.useDomainAllowsQuery;
export {
useDomainBlocksQuery,
useDomainAllowsQuery,
};

View File

@ -0,0 +1,140 @@
/*
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 { replaceCacheOnMutation } from "../../query-modifiers";
import { gtsApi } from "../../gts-api";
import {
type DomainPerm,
type ImportDomainPermsParams,
type MappedDomainPerms,
isDomainPermInternalKey,
} from "../../../types/domain-permission";
import { listToKeyedObject } from "../../transforms";
/**
* Builds up a map function that can be applied to a
* list of DomainPermission entries in order to normalize
* them before submission to the API.
* @param formData
* @returns
*/
function importEntriesProcessor(formData: ImportDomainPermsParams): (_entry: DomainPerm) => DomainPerm {
let processingFuncs: { (_entry: DomainPerm): void; }[] = [];
// Override each obfuscate entry if necessary.
if (formData.obfuscate !== undefined) {
const obfuscateEntry = (entry: DomainPerm) => {
entry.obfuscate = formData.obfuscate;
};
processingFuncs.push(obfuscateEntry);
}
// Check whether we need to append or replace
// private_comment and public_comment.
["private_comment","public_comment"].forEach((commentType) => {
let text = formData.commentType?.trim();
if (!text) {
return;
}
switch(formData[`${commentType}_behavior`]) {
case "append":
const appendComment = (entry: DomainPerm) => {
if (entry.commentType == undefined) {
entry.commentType = text;
} else {
entry.commentType = [entry.commentType, text].join("\n");
}
};
processingFuncs.push(appendComment);
break;
case "replace":
const replaceComment = (entry: DomainPerm) => {
entry.commentType = text;
};
processingFuncs.push(replaceComment);
break;
}
});
return function process(entry) {
// Call all the assembled processing functions.
processingFuncs.forEach((f) => f(entry));
// Unset all internal processing keys
// and any undefined keys on this entry.
Object.entries(entry).forEach(([key, val]: [keyof DomainPerm, any]) => {
if (val == undefined || isDomainPermInternalKey(key)) {
delete entry[key];
}
});
return entry;
};
}
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
importDomainPerms: build.mutation<MappedDomainPerms, ImportDomainPermsParams>({
query: (formData) => {
// Add/replace comments, remove internal keys.
const process = importEntriesProcessor(formData);
const domains = formData.domains.map(process);
return {
method: "POST",
url: `/api/v1/admin/domain_${formData.permType}s?import=true`,
asForm: true,
discardEmpty: true,
body: {
import: true,
domains: new Blob(
[JSON.stringify(domains)],
{ type: "application/json" },
),
}
};
},
transformResponse: listToKeyedObject<DomainPerm>("domain"),
...replaceCacheOnMutation((formData: ImportDomainPermsParams) => {
// Query names for blocks and allows are like
// `domainBlocks` and `domainAllows`, so we need
// to convert `block` -> `Block` or `allow` -> `Allow`
// to do proper cache invalidation.
const permType =
formData.permType.charAt(0).toUpperCase() +
formData.permType.slice(1);
return `domain${permType}s`;
}),
})
})
});
/**
* POST domain permissions to /api/v1/admin/domain_{permType}s.
* Returns the newly created permissions.
*/
const useImportDomainPermsMutation = extended.useImportDomainPermsMutation;
export {
useImportDomainPermsMutation,
};

View File

@ -0,0 +1,163 @@
/*
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 {
ParseConfig as CSVParseConfig,
parse as csvParse
} from "papaparse";
import { nanoid } from "nanoid";
import { isValidDomainPermission, hasBetterScope } from "../../../util/domain-permission";
import { gtsApi } from "../../gts-api";
import {
isDomainPerms,
type DomainPerm,
} from "../../../types/domain-permission";
/**
* Parse the given string of domain permissions and return it as an array.
* Accepts input as a JSON array string, a CSV, or newline-separated domain names.
* Will throw an error if input is invalid.
* @param list
* @returns
* @throws
*/
function parseDomainList(list: string): DomainPerm[] {
if (list.startsWith("[")) {
// Assume JSON array.
const data = JSON.parse(list);
if (!isDomainPerms(data)) {
throw "parsed JSON was not array of DomainPermission";
}
return data;
} else if (list.startsWith("#domain") || list.startsWith("domain,severity")) {
// Assume Mastodon-style CSV.
const csvParseCfg: CSVParseConfig = {
header: true,
// Remove leading '#' if present.
transformHeader: (header) => header.startsWith("#") ? header.slice(1) : header,
skipEmptyLines: true,
dynamicTyping: true
};
const { data, errors } = csvParse(list, csvParseCfg);
if (errors.length > 0) {
let error = "";
errors.forEach((err) => {
error += `${err.message} (line ${err.row})`;
});
throw error;
}
if (!isDomainPerms(data)) {
throw "parsed CSV was not array of DomainPermission";
}
return data;
} else {
// Fallback: assume newline-separated
// list of simple domain strings.
const data: DomainPerm[] = [];
list.split("\n").forEach((line) => {
let domain = line.trim();
let valid = true;
if (domain.startsWith("http")) {
try {
domain = new URL(domain).hostname;
} catch (e) {
valid = false;
}
}
if (domain.length > 0) {
data.push({ domain, valid });
}
});
return data;
}
}
function deduplicateDomainList(list: DomainPerm[]): DomainPerm[] {
let domains = new Set();
return list.filter((entry) => {
if (domains.has(entry.domain)) {
return false;
} else {
domains.add(entry.domain);
return true;
}
});
}
function validateDomainList(list: DomainPerm[]) {
list.forEach((entry) => {
if (entry.domain.startsWith("*.")) {
// A domain permission always includes
// all subdomains, wildcard is meaningless here
entry.domain = entry.domain.slice(2);
}
entry.valid = (entry.valid !== false) && isValidDomainPermission(entry.domain);
if (entry.valid) {
entry.suggest = hasBetterScope(entry.domain);
}
entry.checked = entry.valid;
});
return list;
}
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
processDomainPermissions: build.mutation<DomainPerm[], any>({
async queryFn(formData, _api, _extraOpts, _fetchWithBQ) {
if (formData.domains == undefined || formData.domains.length == 0) {
throw "No domains entered";
}
// Parse + tidy up the form data.
const permissions = parseDomainList(formData.domains);
const deduped = deduplicateDomainList(permissions);
const validated = validateDomainList(deduped);
validated.forEach((entry) => {
// Set unique key that stays stable
// even if domain gets modified by user.
entry.key = nanoid();
});
return { data: validated };
}
})
})
});
/**
* useProcessDomainPermissionsMutation uses the RTK Query API without actually
* hitting the GtS API, it's purely an internal function for our own convenience.
*
* It returns the validated and deduplicated domain permission list.
*/
const useProcessDomainPermissionsMutation = extended.useProcessDomainPermissionsMutation;
export { useProcessDomainPermissionsMutation };

View File

@ -0,0 +1,109 @@
/*
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 { gtsApi } from "../../gts-api";
import {
replaceCacheOnMutation,
removeFromCacheOnMutation,
} from "../../query-modifiers";
import { listToKeyedObject } from "../../transforms";
import type {
DomainPerm,
MappedDomainPerms
} from "../../../types/domain-permission";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
addDomainBlock: build.mutation<MappedDomainPerms, any>({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/domain_blocks`,
asForm: true,
body: formData,
discardEmpty: true
}),
transformResponse: listToKeyedObject<DomainPerm>("domain"),
...replaceCacheOnMutation("domainBlocks"),
}),
addDomainAllow: build.mutation<MappedDomainPerms, any>({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/domain_allows`,
asForm: true,
body: formData,
discardEmpty: true
}),
transformResponse: listToKeyedObject<DomainPerm>("domain"),
...replaceCacheOnMutation("domainAllows")
}),
removeDomainBlock: build.mutation<DomainPerm, string>({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/domain_blocks/${id}`,
}),
...removeFromCacheOnMutation("domainBlocks", {
key: (_draft, newData) => {
return newData.domain;
}
})
}),
removeDomainAllow: build.mutation<DomainPerm, string>({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/domain_allows/${id}`,
}),
...removeFromCacheOnMutation("domainAllows", {
key: (_draft, newData) => {
return newData.domain;
}
})
}),
}),
});
/**
* Add a single domain permission (block) by POSTing to `/api/v1/admin/domain_blocks`.
*/
const useAddDomainBlockMutation = extended.useAddDomainBlockMutation;
/**
* Add a single domain permission (allow) by POSTing to `/api/v1/admin/domain_allows`.
*/
const useAddDomainAllowMutation = extended.useAddDomainAllowMutation;
/**
* Remove a single domain permission (block) by DELETEing to `/api/v1/admin/domain_blocks/{id}`.
*/
const useRemoveDomainBlockMutation = extended.useRemoveDomainBlockMutation;
/**
* Remove a single domain permission (allow) by DELETEing to `/api/v1/admin/domain_allows/{id}`.
*/
const useRemoveDomainAllowMutation = extended.useRemoveDomainAllowMutation;
export {
useAddDomainBlockMutation,
useAddDomainAllowMutation,
useRemoveDomainBlockMutation,
useRemoveDomainAllowMutation
};

View File

@ -1,264 +0,0 @@
/*
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/>.
*/
const Promise = require("bluebird");
const fileDownload = require("js-file-download");
const csv = require("papaparse");
const { nanoid } = require("nanoid");
const { isValidDomainBlock, hasBetterScope } = require("../../domain-block");
const {
replaceCacheOnMutation,
domainListToObject,
unwrapRes
} = require("../lib");
function parseDomainList(list) {
if (list[0] == "[") {
return JSON.parse(list);
} else if (list.startsWith("#domain")) { // Mastodon CSV
const { data, errors } = csv.parse(list, {
header: true,
transformHeader: (header) => header.slice(1), // removes starting '#'
skipEmptyLines: true,
dynamicTyping: true
});
if (errors.length > 0) {
let error = "";
errors.forEach((err) => {
error += `${err.message} (line ${err.row})`;
});
throw error;
}
return data;
} else {
return list.split("\n").map((line) => {
let domain = line.trim();
let valid = true;
if (domain.startsWith("http")) {
try {
domain = new URL(domain).hostname;
} catch (e) {
valid = false;
}
}
return domain.length > 0
? { domain, valid }
: null;
}).filter((a) => a); // not `null`
}
}
function validateDomainList(list) {
list.forEach((entry) => {
if (entry.domain.startsWith("*.")) {
// domain block always includes all subdomains, wildcard is meaningless here
entry.domain = entry.domain.slice(2);
}
entry.valid = (entry.valid !== false) && isValidDomainBlock(entry.domain);
if (entry.valid) {
entry.suggest = hasBetterScope(entry.domain);
}
entry.checked = entry.valid;
});
return list;
}
function deduplicateDomainList(list) {
let domains = new Set();
return list.filter((entry) => {
if (domains.has(entry.domain)) {
return false;
} else {
domains.add(entry.domain);
return true;
}
});
}
module.exports = (build) => ({
processDomainList: build.mutation({
queryFn: (formData) => {
return Promise.try(() => {
if (formData.domains == undefined || formData.domains.length == 0) {
throw "No domains entered";
}
return parseDomainList(formData.domains);
}).then((parsed) => {
return deduplicateDomainList(parsed);
}).then((deduped) => {
return validateDomainList(deduped);
}).then((data) => {
data.forEach((entry) => {
entry.key = nanoid(); // unique id that stays stable even if domain gets modified by user
});
return { data };
}).catch((e) => {
return { error: e.toString() };
});
}
}),
exportDomainList: build.mutation({
queryFn: (formData, api, _extraOpts, baseQuery) => {
let process;
if (formData.exportType == "json") {
process = {
transformEntry: (entry) => ({
domain: entry.domain,
public_comment: entry.public_comment,
obfuscate: entry.obfuscate
}),
stringify: (list) => JSON.stringify(list),
extension: ".json",
mime: "application/json"
};
} else if (formData.exportType == "csv") {
process = {
transformEntry: (entry) => [
entry.domain,
"suspend", // severity
false, // reject_media
false, // reject_reports
entry.public_comment,
entry.obfuscate ?? false
],
stringify: (list) => csv.unparse({
fields: "#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate".split(","),
data: list
}),
extension: ".csv",
mime: "text/csv"
};
} else {
process = {
transformEntry: (entry) => entry.domain,
stringify: (list) => list.join("\n"),
extension: ".txt",
mime: "text/plain"
};
}
return Promise.try(() => {
return baseQuery({
url: `/api/v1/admin/domain_blocks`
});
}).then(unwrapRes).then((blockedInstances) => {
return blockedInstances.map(process.transformEntry);
}).then((exportList) => {
return process.stringify(exportList);
}).then((exportAsString) => {
if (formData.action == "export") {
return {
data: exportAsString
};
} else if (formData.action == "export-file") {
let domain = new URL(api.getState().oauth.instance).host;
let date = new Date();
let filename = [
domain,
"blocklist",
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, "0"),
date.getDate().toString().padStart(2, "0"),
].join("-");
fileDownload(
exportAsString,
filename + process.extension,
process.mime
);
}
return { data: null };
}).catch((e) => {
return { error: e };
});
}
}),
importDomainList: build.mutation({
query: (formData) => {
const { domains } = formData;
// add/replace comments, obfuscation data
let process = entryProcessor(formData);
domains.forEach((entry) => {
process(entry);
});
return {
method: "POST",
url: `/api/v1/admin/domain_blocks?import=true`,
asForm: true,
discardEmpty: true,
body: {
domains: new Blob([JSON.stringify(domains)], { type: "application/json" })
}
};
},
transformResponse: domainListToObject,
...replaceCacheOnMutation("instanceBlocks")
})
});
const internalKeys = new Set("key,suggest,valid,checked".split(","));
function entryProcessor(formData) {
let funcs = [];
["private_comment", "public_comment"].forEach((type) => {
let text = formData[type].trim();
if (text.length > 0) {
let behavior = formData[`${type}_behavior`];
if (behavior == "append") {
funcs.push(function appendComment(entry) {
if (entry[type] == undefined) {
entry[type] = text;
} else {
entry[type] = [entry[type], text].join("\n");
}
});
} else if (behavior == "replace") {
funcs.push(function replaceComment(entry) {
entry[type] = text;
});
}
}
});
return function process(entry) {
funcs.forEach((func) => {
func(entry);
});
entry.obfuscate = formData.obfuscate;
Object.entries(entry).forEach(([key, val]) => {
if (internalKeys.has(key) || val == undefined) {
delete entry[key];
}
});
};
}

View File

@ -1,165 +0,0 @@
/*
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/>.
*/
const {
replaceCacheOnMutation,
removeFromCacheOnMutation,
domainListToObject,
idListToObject
} = require("../lib");
const { gtsApi } = require("../gts-api");
const endpoints = (build) => ({
updateInstance: build.mutation({
query: (formData) => ({
method: "PATCH",
url: `/api/v1/instance`,
asForm: true,
body: formData,
discardEmpty: true
}),
...replaceCacheOnMutation("instance")
}),
mediaCleanup: build.mutation({
query: (days) => ({
method: "POST",
url: `/api/v1/admin/media_cleanup`,
params: {
remote_cache_days: days
}
})
}),
instanceKeysExpire: build.mutation({
query: (domain) => ({
method: "POST",
url: `/api/v1/admin/domain_keys_expire`,
params: {
domain: domain
}
})
}),
instanceBlocks: build.query({
query: () => ({
url: `/api/v1/admin/domain_blocks`
}),
transformResponse: domainListToObject
}),
addInstanceBlock: build.mutation({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/domain_blocks`,
asForm: true,
body: formData,
discardEmpty: true
}),
transformResponse: (data) => {
return {
[data.domain]: data
};
},
...replaceCacheOnMutation("instanceBlocks")
}),
removeInstanceBlock: build.mutation({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/domain_blocks/${id}`,
}),
...removeFromCacheOnMutation("instanceBlocks", {
findKey: (_draft, newData) => {
return newData.domain;
}
})
}),
getAccount: build.query({
query: (id) => ({
url: `/api/v1/accounts/${id}`
}),
providesTags: (_, __, id) => [{ type: "Account", id }]
}),
actionAccount: build.mutation({
query: ({ id, action, reason }) => ({
method: "POST",
url: `/api/v1/admin/accounts/${id}/action`,
asForm: true,
body: {
type: action,
text: reason
}
}),
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
}),
searchAccount: build.mutation({
query: (username) => ({
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
}),
transformResponse: (res) => {
return res.accounts ?? [];
}
}),
instanceRules: build.query({
query: () => ({
url: `/api/v1/admin/instance/rules`
}),
transformResponse: idListToObject
}),
addInstanceRule: build.mutation({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/instance/rules`,
asForm: true,
body: formData,
discardEmpty: true
}),
transformResponse: (data) => {
return {
[data.id]: data
};
},
...replaceCacheOnMutation("instanceRules")
}),
updateInstanceRule: build.mutation({
query: ({ id, ...edit }) => ({
method: "PATCH",
url: `/api/v1/admin/instance/rules/${id}`,
asForm: true,
body: edit,
discardEmpty: true
}),
transformResponse: (data) => {
return {
[data.id]: data
};
},
...replaceCacheOnMutation("instanceRules")
}),
deleteInstanceRule: build.mutation({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/instance/rules/${id}`
}),
...removeFromCacheOnMutation("instanceRules", {
findKey: (_draft, rule) => rule.id
})
}),
...require("./import-export")(build),
...require("./custom-emoji")(build),
...require("./reports")(build)
});
module.exports = gtsApi.injectEndpoints({ endpoints });

View File

@ -0,0 +1,148 @@
/*
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 { replaceCacheOnMutation, removeFromCacheOnMutation } from "../query-modifiers";
import { gtsApi } from "../gts-api";
import { listToKeyedObject } from "../transforms";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
updateInstance: build.mutation({
query: (formData) => ({
method: "PATCH",
url: `/api/v1/instance`,
asForm: true,
body: formData,
discardEmpty: true
}),
...replaceCacheOnMutation("instanceV1"),
}),
mediaCleanup: build.mutation({
query: (days) => ({
method: "POST",
url: `/api/v1/admin/media_cleanup`,
params: {
remote_cache_days: days
}
})
}),
instanceKeysExpire: build.mutation({
query: (domain) => ({
method: "POST",
url: `/api/v1/admin/domain_keys_expire`,
params: {
domain: domain
}
})
}),
getAccount: build.query({
query: (id) => ({
url: `/api/v1/accounts/${id}`
}),
providesTags: (_, __, id) => [{ type: "Account", id }]
}),
actionAccount: build.mutation({
query: ({ id, action, reason }) => ({
method: "POST",
url: `/api/v1/admin/accounts/${id}/action`,
asForm: true,
body: {
type: action,
text: reason
}
}),
invalidatesTags: (_, __, { id }) => [{ type: "Account", id }]
}),
searchAccount: build.mutation({
query: (username) => ({
url: `/api/v2/search?q=${encodeURIComponent(username)}&resolve=true`
}),
transformResponse: (res) => {
return res.accounts ?? [];
}
}),
instanceRules: build.query({
query: () => ({
url: `/api/v1/admin/instance/rules`
}),
transformResponse: listToKeyedObject<any>("id")
}),
addInstanceRule: build.mutation({
query: (formData) => ({
method: "POST",
url: `/api/v1/admin/instance/rules`,
asForm: true,
body: formData,
discardEmpty: true
}),
transformResponse: (data) => {
return {
[data.id]: data
};
},
...replaceCacheOnMutation("instanceRules"),
}),
updateInstanceRule: build.mutation({
query: ({ id, ...edit }) => ({
method: "PATCH",
url: `/api/v1/admin/instance/rules/${id}`,
asForm: true,
body: edit,
discardEmpty: true
}),
transformResponse: (data) => {
return {
[data.id]: data
};
},
...replaceCacheOnMutation("instanceRules"),
}),
deleteInstanceRule: build.mutation({
query: (id) => ({
method: "DELETE",
url: `/api/v1/admin/instance/rules/${id}`
}),
...removeFromCacheOnMutation("instanceRules", {
key: (_draft, rule) => rule.id,
})
})
})
});
export const {
useUpdateInstanceMutation,
useMediaCleanupMutation,
useInstanceKeysExpireMutation,
useGetAccountQuery,
useActionAccountMutation,
useSearchAccountMutation,
useInstanceRulesQuery,
useAddInstanceRuleMutation,
useUpdateInstanceRuleMutation,
useDeleteInstanceRuleMutation,
} = extended;

View File

@ -1,51 +0,0 @@
/*
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/>.
*/
module.exports = (build) => ({
listReports: build.query({
query: (params = {}) => ({
url: "/api/v1/admin/reports",
params: {
limit: 100,
...params
}
}),
providesTags: ["Reports"]
}),
getReport: build.query({
query: (id) => ({
url: `/api/v1/admin/reports/${id}`
}),
providesTags: (res, error, id) => [{ type: "Reports", id }]
}),
resolveReport: build.mutation({
query: (formData) => ({
url: `/api/v1/admin/reports/${formData.id}/resolve`,
method: "POST",
asForm: true,
body: formData
}),
invalidatesTags: (res) =>
res
? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }]
: [{ type: "Reports", id: "LIST" }]
})
});

View File

@ -0,0 +1,83 @@
/*
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 { gtsApi } from "../../gts-api";
import type {
AdminReport,
AdminReportListParams,
AdminReportResolveParams,
} from "../../../types/report";
const extended = gtsApi.injectEndpoints({
endpoints: (build) => ({
listReports: build.query<AdminReport[], AdminReportListParams | void>({
query: (params) => ({
url: "/api/v1/admin/reports",
params: {
// Override provided limit.
limit: 100,
...params
}
}),
providesTags: ["Reports"]
}),
getReport: build.query<AdminReport, string>({
query: (id) => ({
url: `/api/v1/admin/reports/${id}`
}),
providesTags: (_res, _error, id) => [{ type: "Reports", id }]
}),
resolveReport: build.mutation<AdminReport, AdminReportResolveParams>({
query: (formData) => ({
url: `/api/v1/admin/reports/${formData.id}/resolve`,
method: "POST",
asForm: true,
body: formData
}),
invalidatesTags: (res) =>
res
? [{ type: "Reports", id: "LIST" }, { type: "Reports", id: res.id }]
: [{ type: "Reports", id: "LIST" }]
})
})
});
/**
* List reports received on this instance, filtered using given parameters.
*/
const useListReportsQuery = extended.useListReportsQuery;
/**
* Get a single report by its ID.
*/
const useGetReportQuery = extended.useGetReportQuery;
/**
* Mark an open report as resolved.
*/
const useResolveReportMutation = extended.useResolveReportMutation;
export {
useListReportsQuery,
useGetReportQuery,
useResolveReportMutation,
};

View File

@ -26,6 +26,7 @@ import type {
import { serialize as serializeForm } from "object-to-formdata";
import type { RootState } from '../../redux/store';
import { InstanceV1 } from '../types/instance';
/**
* GTSFetchArgs extends standard FetchArgs used by
@ -72,7 +73,7 @@ const gtsBaseQuery: BaseQueryFn<
const { instanceUrl, token } = state.oauth;
// Derive baseUrl dynamically.
let baseUrl: string;
let baseUrl: string | undefined;
// Check if simple string baseUrl provided
// as args, or if more complex args provided.
@ -137,8 +138,8 @@ export const gtsApi = createApi({
"Account",
"InstanceRules",
],
endpoints: (builder) => ({
instance: builder.query<any, void>({
endpoints: (build) => ({
instanceV1: build.query<InstanceV1, void>({
query: () => ({
url: `/api/v1/instance`
})
@ -146,4 +147,11 @@ export const gtsApi = createApi({
})
});
export const { useInstanceQuery } = gtsApi;
/**
* Query /api/v1/instance to retrieve basic instance information.
* This endpoint does not require authentication/authorization.
* TODO: move this to ./instance.
*/
const useInstanceV1Query = gtsApi.useInstanceV1Query;
export { useInstanceV1Query };

View File

@ -1,81 +0,0 @@
/*
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/>.
*/
const syncpipe = require("syncpipe");
const { gtsApi } = require("./gts-api");
module.exports = {
unwrapRes(res) {
if (res.error != undefined) {
throw res.error;
} else {
return res.data;
}
},
domainListToObject: (data) => {
// Turn flat Array into Object keyed by block's domain
return syncpipe(data, [
(_) => _.map((entry) => [entry.domain, entry]),
(_) => Object.fromEntries(_)
]);
},
idListToObject: (data) => {
// Turn flat Array into Object keyed by entry id field
return syncpipe(data, [
(_) => _.map((entry) => [entry.id, entry]),
(_) => Object.fromEntries(_)
]);
},
replaceCacheOnMutation: makeCacheMutation((draft, newData) => {
Object.assign(draft, newData);
}),
appendCacheOnMutation: makeCacheMutation((draft, newData) => {
draft.push(newData);
}),
spliceCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
draft.splice(key, 1);
}),
updateCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
draft[key] = newData;
}),
removeFromCacheOnMutation: makeCacheMutation((draft, newData, { key }) => {
delete draft[key];
}),
editCacheOnMutation: makeCacheMutation((draft, newData, { update }) => {
update(draft, newData);
})
};
// https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
function makeCacheMutation(action) {
return function cacheMutation(queryName, { key, findKey, arg, ...opts } = {}) {
return {
onQueryStarted: (_, { dispatch, queryFulfilled }) => {
queryFulfilled.then(({ data: newData }) => {
dispatch(gtsApi.util.updateQueryData(queryName, arg, (draft) => {
if (findKey != undefined) {
key = findKey(draft, newData);
}
action(draft, newData, { key, ...opts });
}));
});
}
};
};
}

View File

@ -57,8 +57,8 @@ const SETTINGS_URL = (getSettingsURL());
//
// https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#performing-multiple-requests-with-a-single-query
const extended = gtsApi.injectEndpoints({
endpoints: (builder) => ({
verifyCredentials: builder.query<any, void>({
endpoints: (build) => ({
verifyCredentials: build.query<any, void>({
providesTags: (_res, error) =>
error == undefined ? ["Auth"] : [],
async queryFn(_arg, api, _extraOpts, fetchWithBQ) {
@ -135,7 +135,7 @@ const extended = gtsApi.injectEndpoints({
}
}),
authorizeFlow: builder.mutation({
authorizeFlow: build.mutation({
async queryFn(formData, api, _extraOpts, fetchWithBQ) {
const state = api.getState() as RootState;
const oauthState = state.oauth;
@ -187,7 +187,7 @@ const extended = gtsApi.injectEndpoints({
return { data: null };
},
}),
logout: builder.mutation({
logout: build.mutation({
queryFn: (_arg, api) => {
api.dispatch(oauthRemove());
return { data: null };
@ -201,4 +201,4 @@ export const {
useVerifyCredentialsQuery,
useAuthorizeFlowMutation,
useLogoutMutation,
} = extended;
} = extended;

View File

@ -0,0 +1,150 @@
/*
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 { gtsApi } from "./gts-api";
import type {
Action,
CacheMutation,
} from "../types/query";
import { NoArg } from "../types/query";
/**
* Cache mutation creator for pessimistic updates.
*
* Feed it a function that you want to perform on the
* given draft and updated data, using the given parameters.
*
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
* https://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates#pessimistic-updates
*/
function makeCacheMutation(action: Action): CacheMutation {
return function cacheMutation(
queryName: string | ((_arg: any) => string),
{ key } = {},
) {
return {
onQueryStarted: async(mutationData, { dispatch, queryFulfilled }) => {
// queryName might be a function that returns
// a query name; trigger it if so. The returned
// queryName has to match one of the API endpoints
// we've defined. So if we have endpoints called
// (for example) `instanceV1` and `getPosts` then
// the queryName provided here has to line up with
// one of those in order to actually do anything.
if (typeof queryName !== "string") {
queryName = queryName(mutationData);
}
if (queryName == "") {
throw (
"provided queryName resolved to an empty string;" +
"double check your mutation definition!"
);
}
try {
// Wait for the mutation to finish (this
// is why it's a pessimistic update).
const { data: newData } = await queryFulfilled;
// In order for `gtsApi.util.updateQueryData` to
// actually do something within a dispatch, the
// first two arguments passed into it have to line
// up with arguments that were used earlier to
// fetch the data whose cached version we're now
// trying to modify.
//
// So, if we earlier fetched all reports with
// queryName `getReports`, and arg `undefined`,
// then we now need match those parameters in
// `updateQueryData` in order to modify the cache.
//
// If you pass something like `null` or `""` here
// instead, then the cache will not get modified!
// Redux will just quietly discard the thunk action.
dispatch(
gtsApi.util.updateQueryData(queryName as any, NoArg, (draft) => {
if (key != undefined && typeof key !== "string") {
key = key(draft, newData);
}
action(draft, newData, { key });
})
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`rolling back pessimistic update of ${queryName}: ${e}`);
}
}
};
};
}
/**
*
*/
const replaceCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, _params) => {
Object.assign(draft, newData);
});
const appendCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, _params) => {
draft.push(newData);
});
const spliceCacheOnMutation: CacheMutation = makeCacheMutation((draft, _newData, { key }) => {
if (key === undefined) {
throw ("key undefined");
}
draft.splice(key, 1);
});
const updateCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, { key }) => {
if (key === undefined) {
throw ("key undefined");
}
if (typeof key !== "string") {
key = key(draft, newData);
}
draft[key] = newData;
});
const removeFromCacheOnMutation: CacheMutation = makeCacheMutation((draft, newData, { key }) => {
if (key === undefined) {
throw ("key undefined");
}
if (typeof key !== "string") {
key = key(draft, newData);
}
delete draft[key];
});
export {
replaceCacheOnMutation,
appendCacheOnMutation,
spliceCacheOnMutation,
updateCacheOnMutation,
removeFromCacheOnMutation,
};

View File

@ -0,0 +1,78 @@
/*
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/>.
*/
/**
* Map a list of items into an object.
*
* In the following example, a list of DomainPerms like the following:
*
* ```json
* [
* {
* "domain": "example.org",
* "public_comment": "aaaaa!!"
* },
* {
* "domain": "another.domain",
* "public_comment": "they are poo"
* }
* ]
* ```
*
* Would be converted into an Object like the following:
*
* ```json
* {
* "example.org": {
* "domain": "example.org",
* "public_comment": "aaaaa!!"
* },
* "another.domain": {
* "domain": "another.domain",
* "public_comment": "they are poo"
* },
* }
* ```
*
* If you pass a non-array type into this function it
* will be converted into an array first, as a treat.
*
* @example
* const extended = gtsApi.injectEndpoints({
* endpoints: (build) => ({
* getDomainBlocks: build.query<MappedDomainPerms, void>({
* query: () => ({
* url: `/api/v1/admin/domain_blocks`
* }),
* transformResponse: listToKeyedObject<DomainPerm>("domain"),
* }),
* });
*/
export function listToKeyedObject<T>(key: keyof T) {
return (list: T[] | T): { [_ in keyof T]: T } => {
// Ensure we're actually
// dealing with an array.
if (!Array.isArray(list)) {
list = [list];
}
const entries = list.map((entry) => [entry[key], entry]);
return Object.fromEntries(entries);
};
}

View File

@ -17,12 +17,12 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { replaceCacheOnMutation } from "../lib";
import { replaceCacheOnMutation } from "../query-modifiers";
import { gtsApi } from "../gts-api";
const extended = gtsApi.injectEndpoints({
endpoints: (builder) => ({
updateCredentials: builder.mutation({
endpoints: (build) => ({
updateCredentials: build.mutation({
query: (formData) => ({
method: "PATCH",
url: `/api/v1/accounts/update_credentials`,
@ -32,7 +32,7 @@ const extended = gtsApi.injectEndpoints({
}),
...replaceCacheOnMutation("verifyCredentials")
}),
passwordChange: builder.mutation({
passwordChange: build.mutation({
query: (data) => ({
method: "POST",
url: `/api/v1/user/password_change`,

View File

@ -17,25 +17,33 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const React = require("react");
const { Switch, Route } = require("wouter");
export interface CustomEmoji {
id?: string;
shortcode: string;
category?: string;
}
const InstanceOverview = require("./overview");
const InstanceDetail = require("./detail");
const InstanceImportExport = require("./import-export");
/**
* Query parameters for GET to /api/v1/admin/custom_emojis.
*/
export interface ListEmojiParams {
module.exports = function Federation({ baseUrl }) {
return (
<Switch>
<Route path={`${baseUrl}/import-export/:list?`}>
<InstanceImportExport />
</Route>
}
<Route path={`${baseUrl}/:domain`}>
<InstanceDetail baseUrl={baseUrl} />
</Route>
<InstanceOverview baseUrl={baseUrl} />
</Switch>
);
};
/**
* Result of searchItemForEmoji mutation.
*/
export interface EmojisFromItem {
/**
* Type of the search item result.
*/
type: "statuses" | "accounts";
/**
* Domain of the returned emojis.
*/
domain: string;
/**
* Discovered emojis.
*/
list: CustomEmoji[];
}

View File

@ -0,0 +1,97 @@
/*
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 typia from "typia";
export const isDomainPerms = typia.createIs<DomainPerm[]>();
export type PermType = "block" | "allow";
/**
* A single domain permission entry (block or allow).
*/
export interface DomainPerm {
id?: string;
domain: string;
obfuscate?: boolean;
private_comment?: string;
public_comment?: string;
created_at?: string;
// Internal processing keys; remove
// before serdes of domain perm.
key?: string;
permType?: PermType;
suggest?: string;
valid?: boolean;
checked?: boolean;
commentType?: string;
private_comment_behavior?: "append" | "replace";
public_comment_behavior?: "append" | "replace";
}
/**
* Domain permissions mapped to an Object where the Object
* keys are the "domain" value of each DomainPerm.
*/
export interface MappedDomainPerms {
[key: string]: DomainPerm;
}
const domainPermInternalKeys: Set<keyof DomainPerm> = new Set([
"key",
"permType",
"suggest",
"valid",
"checked",
"commentType",
"private_comment_behavior",
"public_comment_behavior",
]);
/**
* Returns true if provided DomainPerm Object key is
* "internal"; ie., it's just for our use, and it shouldn't
* be serialized to or deserialized from the GtS API.
*
* @param key
* @returns
*/
export function isDomainPermInternalKey(key: keyof DomainPerm) {
return domainPermInternalKeys.has(key);
}
export interface ImportDomainPermsParams {
domains: DomainPerm[];
// Internal processing keys;
// remove before serdes of form.
obfuscate?: boolean;
commentType?: string;
permType: PermType;
}
/**
* Model domain permissions bulk export params.
*/
export interface ExportDomainPermsParams {
permType: PermType;
action: "export" | "export-file";
exportType: "json" | "csv" | "plain";
}

View File

@ -0,0 +1,91 @@
/*
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/>.
*/
export interface InstanceV1 {
uri: string;
account_domain: string;
title: string;
description: string;
short_description: string;
email: string;
version: string;
languages: any[]; // TODO: define this
registrations: boolean;
approval_required: boolean;
invites_enabled: boolean;
configuration: InstanceConfiguration;
urls: InstanceUrls;
stats: InstanceStats;
thumbnail: string;
contact_account: Object; // TODO: define this.
max_toot_chars: number;
rules: any[]; // TODO: define this
}
export interface InstanceConfiguration {
statuses: InstanceStatuses;
media_attachments: InstanceMediaAttachments;
polls: InstancePolls;
accounts: InstanceAccounts;
emojis: InstanceEmojis;
}
export interface InstanceAccounts {
allow_custom_css: boolean;
max_featured_tags: number;
max_profile_fields: number;
}
export interface InstanceEmojis {
emoji_size_limit: number;
}
export interface InstanceMediaAttachments {
supported_mime_types: string[];
image_size_limit: number;
image_matrix_limit: number;
video_size_limit: number;
video_frame_rate_limit: number;
video_matrix_limit: number;
}
export interface InstancePolls {
max_options: number;
max_characters_per_option: number;
min_expiration: number;
max_expiration: number;
}
export interface InstanceStatuses {
max_characters: number;
max_media_attachments: number;
characters_reserved_per_url: number;
supported_mime_types: string[];
}
export interface InstanceStats {
domain_count: number;
status_count: number;
user_count: number;
}
export interface InstanceUrls {
streaming_api: string;
}

View File

@ -0,0 +1,95 @@
/*
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 { Draft } from "@reduxjs/toolkit";
/**
* Pass into a query when you don't
* want to provide an argument to it.
*/
export const NoArg = undefined;
/**
* Shadow the redux onQueryStarted function for mutations.
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
*/
type OnMutationStarted = (
_arg: any,
_params: MutationStartedParams
) => Promise<void>;
/**
* Shadow the redux onQueryStarted function parameters for mutations.
* https://redux-toolkit.js.org/rtk-query/api/createApi#onquerystarted
*/
interface MutationStartedParams {
/**
* The dispatch method for the store.
*/
dispatch,
/**
* A method to get the current state for the store.
*/
getState,
/**
* extra as provided as thunk.extraArgument to the configureStore getDefaultMiddleware option.
*/
extra,
/**
* A unique ID generated for the query/mutation.
*/
requestId,
/**
* A Promise that will resolve with a data property (the transformed query result), and a
* meta property (meta returned by the baseQuery). If the query fails, this Promise will
* reject with the error. This allows you to await for the query to finish.
*/
queryFulfilled,
/**
* A function that gets the current value of the cache entry.
*/
getCacheEntry,
}
export type Action = (
_draft: Draft<any>,
_updated: any,
_params: ActionParams,
) => void;
export interface ActionParams {
/**
* Either a normal old string, or a custom
* function to derive the key to change based
* on the draft and updated data.
*
* @param _draft
* @param _updated
* @returns
*/
key?: string | ((_draft: Draft<any>, _updated: any) => string),
}
/**
* Custom cache mutation.
*/
export type CacheMutation = (
_queryName: string | ((_arg: any) => string),
_params?: ActionParams,
) => { onQueryStarted: OnMutationStarted }

View File

@ -0,0 +1,144 @@
/*
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/>.
*/
/**
* Admin model of a report. Differs from the client
* model, which contains less detailed information.
*/
export interface AdminReport {
/**
* ID of the report.
*/
id: string;
/**
* Whether an action has been taken by an admin in response to this report.
*/
action_taken: boolean;
/**
* Time action was taken, if at all.
*/
action_taken_at?: string;
/**
* Category under which this report was created.
*/
category: string;
/**
* Comment submitted by the report creator.
*/
comment: string;
/**
* Report was/should be federated to remote instance.
*/
forwarded: boolean;
/**
* Time when the report was created.
*/
created_at: string;
/**
* Time when the report was last updated.
*/
updated_at: string;
/**
* Account that created the report.
* TODO: model this properly.
*/
account: Object;
/**
* Reported account.
* TODO: model this properly.
*/
target_account: Object;
/**
* Admin account assigned to handle this report, if any.
* TODO: model this properly.
*/
assigned_account?: Object;
/**
* Admin account that has taken action on this report, if any.
* TODO: model this properly.
*/
action_taken_by_account?: Object;
/**
* Statuses cited by this report, if any.
* TODO: model this properly.
*/
statuses: Object[];
/**
* Rules broken according to the reporter, if any.
* TODO: model this properly.
*/
rules: Object[];
/**
* Comment stored about what action (if any) was taken.
*/
action_taken_comment?: string;
}
/**
* Parameters for POST to /api/v1/admin/reports/{id}/resolve.
*/
export interface AdminReportResolveParams {
/**
* The ID of the report to resolve.
*/
id: string;
/**
* Comment to store about what action (if any) was taken.
* Will be shown to the user who created the report (if local).
*/
action_taken_comment?: string;
}
/**
* Parameters for GET to /api/v1/admin/reports.
*/
export interface AdminReportListParams {
/**
* If set, show only resolved (true) or only unresolved (false) reports.
*/
resolved?: boolean;
/**
* If set, show only reports created by the given account ID.
*/
account_id?: string;
/**
* If set, show only reports that target the given account ID.
*/
target_account_id?: string;
/**
* If set, show only reports older (ie., lower) than the given ID.
* Report with the given ID will not be included in response.
*/
max_id?: string;
/**
* If set, show only reports newer (ie., higher) than the given ID.
* Report with the given ID will not be included in response.
*/
since_id?: string;
/**
* If set, show only reports *immediately newer* than the given ID.
* Report with the given ID will not be included in response.
*/
min_id?: string;
/**
* If set, limit returned reports to this number.
* Else, fall back to GtS API defaults.
*/
limit?: number;
}

View File

@ -17,33 +17,32 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
const isValidDomain = require("is-valid-domain");
const psl = require("psl");
import isValidDomain from "is-valid-domain";
import { get } from "psl";
function isValidDomainBlock(domain) {
/**
* Check the input string to ensure it's a valid
* domain that doesn't include a wildcard ("*").
* @param domain
* @returns
*/
export function isValidDomainPermission(domain: string): boolean {
return isValidDomain(domain, {
/*
Wildcard prefix *. can be stripped since it's equivalent to not having it,
but wildcard anywhere else in the domain is not handled by the backend so it's invalid.
*/
wildcard: false,
allowUnicode: true
});
}
/*
Still can't think of a better function name for this,
but we're checking a domain against the Public Suffix List <https://publicsuffix.org/>
to see if we should suggest removing subdomain(s) since they're likely owned/ran by the same party
social.example.com -> suggests example.com
*/
function hasBetterScope(domain) {
const lookup = psl.get(domain);
/**
* Checks a domain against the Public Suffix List <https://publicsuffix.org/> to see if we
* should suggest removing subdomain(s), since they're likely owned/ran by the same party.
* Eg., "social.example.com" suggests "example.com".
* @param domain
* @returns
*/
export function hasBetterScope(domain: string): string | undefined {
const lookup = get(domain);
if (lookup && lookup != domain) {
return lookup;
} else {
return false;
}
}
module.exports = { isValidDomainBlock, hasBetterScope };

View File

@ -498,7 +498,7 @@ span.form-info {
}
}
.instance-list {
.domain-permissions-list {
p {
margin-top: 0;
}
@ -612,7 +612,7 @@ span.form-info {
padding: 0.75rem;
}
.instance-list .filter {
.domain-permissions-list .filter {
flex-direction: column;
}
}
@ -809,7 +809,7 @@ button.with-padding {
animation-fill-mode: forwards;
}
.suspend-import-list {
.domain-perm-import-list {
.checkbox-list-wrapper {
overflow-x: auto;
display: grid;
@ -844,7 +844,7 @@ button.with-padding {
#icon {
align-self: center;
.already-blocked {
.permission-already-exists {
color: $green1;
}
@ -875,6 +875,12 @@ button.with-padding {
align-items: center;
}
.form-field.radio {
display: flex;
flex-direction: column;
margin-left: 0.5rem;
}
.button-grid {
display: inline-grid;
grid-template-columns: auto auto auto;

View File

@ -19,8 +19,6 @@
const React = require("react");
const query = require("../lib/query");
const {
useTextInput,
useFileInput,
@ -28,7 +26,7 @@ const {
useFieldArrayInput
} = require("../lib/form");
const useFormSubmit = require("../lib/form/submit");
const useFormSubmit = require("../lib/form/submit").default;
const { useWithFormContext, FormContext } = require("../lib/form/context");
const {
@ -38,14 +36,18 @@ const {
Checkbox
} = require("../components/form/inputs");
const FormWithData = require("../lib/form/form-with-data");
const FormWithData = require("../lib/form/form-with-data").default;
const FakeProfile = require("../components/fake-profile");
const MutationButton = require("../components/form/mutation-button");
const { useInstanceV1Query } = require("../lib/query");
const { useUpdateCredentialsMutation } = require("../lib/query/user");
const { useVerifyCredentialsQuery } = require("../lib/query/oauth");
module.exports = function UserProfile() {
return (
<FormWithData
dataQuery={query.useVerifyCredentialsQuery}
dataQuery={useVerifyCredentialsQuery}
DataForm={UserProfileForm}
/>
);
@ -64,7 +66,7 @@ function UserProfileForm({ data: profile }) {
- string custom_css (if enabled)
*/
const { data: instance } = query.useInstanceQuery();
const { data: instance } = useInstanceV1Query();
const instanceConfig = React.useMemo(() => {
return {
allowCustomCSS: instance?.configuration?.accounts?.allow_custom_css === true,
@ -88,7 +90,7 @@ function UserProfileForm({ data: profile }) {
}),
};
const [submitForm, result] = useFormSubmit(form, query.useUpdateCredentialsMutation(), {
const [submitForm, result] = useFormSubmit(form, useUpdateCredentialsMutation(), {
onFinish: () => {
form.avatar.reset();
form.header.reset();

View File

@ -26,7 +26,7 @@ const {
useBoolInput
} = require("../lib/form");
const useFormSubmit = require("../lib/form/submit");
const useFormSubmit = require("../lib/form/submit").default;
const {
Select,
@ -34,7 +34,7 @@ const {
Checkbox
} = require("../components/form/inputs");
const FormWithData = require("../lib/form/form-with-data");
const FormWithData = require("../lib/form/form-with-data").default;
const Languages = require("../components/languages");
const MutationButton = require("../components/form/mutation-button");

View File

@ -84,7 +84,7 @@
/* Type Checking */
"strict": false, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
"strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
@ -104,6 +104,10 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"plugins": [
{ "transform": "typia/lib/transform" }
]
}
}

View File

@ -1040,6 +1040,13 @@
through2 "^4.0.2"
xtend "^4.0.1"
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -1132,7 +1139,7 @@
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/resolve-uri@^3.1.0":
"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
@ -1155,6 +1162,14 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.19"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz#f8a3249862f91be48d3127c3cfe992f79b4b8811"
@ -1194,6 +1209,31 @@
redux-thunk "^2.4.2"
reselect "^4.1.8"
"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
"@types/bluebird@^3.5.39":
version "3.5.39"
resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.39.tgz#6aaf8bcbf005bb091d06ddaa0f620be078bf6a73"
integrity sha512-0h2lKudcFwHih8NHAgt/uyAIUQDO0AdfJYlWBXD8r+gFDulUi2CMZoQSh2Q5ol1FMaHV9k7/4HtcbA8ABtexmA==
"@types/hoist-non-react-statics@^3.3.1":
version "3.3.2"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#dc1e9ded53375d37603c479cc12c693b0878aa2a"
@ -1209,6 +1249,11 @@
dependencies:
"@types/node" "*"
"@types/is-valid-domain@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@types/is-valid-domain/-/is-valid-domain-0.0.2.tgz#78b236f05da281213481c4af0a7ce452d4ff810a"
integrity sha512-18CgqfDjh0m+GFfekGz1q3g32XESx7vutfBFnPkIdpDtuvgvOac8lrghRiw3SLI19vNa/XdPKIhL6CQpFMIDug==
"@types/json-schema@^7.0.12":
version "7.0.13"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85"
@ -1219,11 +1264,23 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4"
integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==
"@types/papaparse@^5.3.9":
version "5.3.9"
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.9.tgz#5f955949eae512c1eec70bba4bfeb2e7f4396564"
integrity sha512-sZcrKD63qA4/6GyBcVvX6AIp0AkpfyYk00CUQHMBvb4+OVXTZWyXUvidUZaai1wyKUVyJoxO7mgREam/pMRrDw==
dependencies:
"@types/node" "*"
"@types/prop-types@*":
version "15.7.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.8.tgz#805eae6e8f41bd19e88917d2ea200dc992f405d3"
integrity sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==
"@types/psl@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/psl/-/psl-1.1.1.tgz#3ba9e6d4bd2a32652a639fd5df7e539151d0a3b2"
integrity sha512-nHPbucWhAfVSuJ+xVc4AjjtM/y6U/eLHeXxyjzPHzKVr+j8uHvGg2wlXjmReSE2p851ltEWKGNQOtBK0beF/Eg==
"@types/react-dom@^18.2.8":
version "18.2.10"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.10.tgz#06247cb600e39b63a0a385f6a5014c44bab296f2"
@ -1781,12 +1838,17 @@ acorn-walk@^7.0.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc"
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^7.0.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.8.2, acorn@^8.9.0:
acorn@^8.4.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
@ -1806,6 +1868,13 @@ amdefine@>=0.0.4:
resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
integrity sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==
ansi-escapes@^4.2.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
dependencies:
type-fest "^0.21.3"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
@ -1818,7 +1887,7 @@ ansi-styles@^3.2.1:
dependencies:
color-convert "^1.9.0"
ansi-styles@^4.1.0:
ansi-styles@^4.0.0, ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@ -1838,6 +1907,11 @@ anymatch@^3.1.0, anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@ -1893,6 +1967,11 @@ array-includes@^3.1.6:
get-intrinsic "^1.2.1"
is-string "^1.0.7"
array-timsort@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/array-timsort/-/array-timsort-1.0.3.tgz#3c9e4199e54fb2b9c3fe5976396a21614ef0d926"
integrity sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
@ -2043,7 +2122,7 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bl@^4.0.0, bl@^4.0.2:
bl@^4.0.0, bl@^4.0.2, bl@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
@ -2343,7 +2422,7 @@ chalk@^2.4.2:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.0:
chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -2351,6 +2430,11 @@ chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chokidar@^3.4.0:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -2374,6 +2458,23 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
dependencies:
restore-cursor "^3.1.0"
cli-spinners@^2.5.0:
version "2.9.1"
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.1.tgz#9c0b9dad69a6d47cbb4333c14319b060ed395a35"
integrity sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==
cli-width@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
clone-regexp@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f"
@ -2381,6 +2482,11 @@ clone-regexp@^2.1.0:
dependencies:
is-regexp "^2.0.0"
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -2425,11 +2531,27 @@ combine-source-map@~0.6.1:
lodash.memoize "~3.0.3"
source-map "~0.4.2"
commander@^10.0.0:
version "10.0.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
comment-json@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
integrity sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==
dependencies:
array-timsort "^1.0.3"
core-util-is "^1.0.3"
esprima "^4.0.1"
has-own-prop "^2.0.0"
repeat-string "^1.6.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2509,7 +2631,7 @@ core-js@^3.26.1:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
integrity sha512-HoZr92+ZjFEKar5HS6MC776gYslNOKHt75mEBKWKnPeFDpZ6nH5OeF3S6HFT1mUAUZKrzkez05VboaX8myjSuw==
core-util-is@~1.0.0:
core-util-is@^1.0.3, core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
@ -2550,6 +2672,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-spawn@^7.0.2:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -2658,6 +2785,13 @@ default-value@^1.0.0:
dependencies:
es6-promise-try "0.0.1"
defaults@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a"
integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==
dependencies:
clone "^1.0.2"
define-data-property@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451"
@ -2732,6 +2866,11 @@ detective@^5.2.0:
defined "^1.0.0"
minimist "^1.2.6"
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
diffie-hellman@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@ -2767,6 +2906,11 @@ domain-browser@^1.2.0:
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
drange@^1.0.2:
version "1.1.1"
resolved "https://registry.yarnpkg.com/drange/-/drange-1.1.1.tgz#b2aecec2aab82fcef11dbbd7b9e32b83f8f6c0b8"
integrity sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==
duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2, duplexer2@~0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@ -2802,6 +2946,11 @@ elliptic@^6.5.3:
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
@ -3231,6 +3380,15 @@ ext@^1.1.2:
dependencies:
type "^2.7.2"
external-editor@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
dependencies:
chardet "^0.7.0"
iconv-lite "^0.4.24"
tmp "^0.0.33"
factor-bundle@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/factor-bundle/-/factor-bundle-2.5.0.tgz#8ea8957da39d7586283cc3ee353cd9911a45e779"
@ -3296,6 +3454,13 @@ faye-websocket@^0.11.3:
dependencies:
websocket-driver ">=0.5.1"
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
dependencies:
escape-string-regexp "^1.0.5"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
@ -3503,6 +3668,15 @@ glob@^7.1.0, glob@^7.1.3:
once "^1.3.0"
path-is-absolute "^1.0.0"
global-prefix@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97"
integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==
dependencies:
ini "^1.3.5"
kind-of "^6.0.2"
which "^1.3.1"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
@ -3566,6 +3740,11 @@ has-flag@^4.0.0:
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
has-own-prop@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-own-prop/-/has-own-prop-2.0.0.tgz#f0f95d58f65804f5d218db32563bb85b8e0417af"
integrity sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==
has-property-descriptors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861"
@ -3674,7 +3853,7 @@ https-browserify@^1.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==
iconv-lite@0.4.24:
iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@ -3779,6 +3958,11 @@ inherits@2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
ini@^1.3.5:
version "1.3.8"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
inject-lr-script@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/inject-lr-script/-/inject-lr-script-2.2.0.tgz#58d91cd99e5de1a3f172aa076f7db8651ee72db2"
@ -3800,6 +3984,27 @@ inline-source-map@~0.6.0:
dependencies:
source-map "~0.5.3"
inquirer@^8.2.5:
version "8.2.6"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562"
integrity sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==
dependencies:
ansi-escapes "^4.2.1"
chalk "^4.1.1"
cli-cursor "^3.1.0"
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
lodash "^4.17.21"
mute-stream "0.0.8"
ora "^5.4.1"
run-async "^2.4.0"
rxjs "^7.5.5"
string-width "^4.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
wrap-ansi "^6.0.1"
insert-css@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4"
@ -3922,6 +4127,11 @@ is-finalizationregistry@^1.0.2:
dependencies:
call-bind "^1.0.2"
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-generator-function@^1.0.10, is-generator-function@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
@ -3936,6 +4146,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-interactive@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
is-map@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
@ -4024,6 +4239,11 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.3, is-typed-
dependencies:
which-typed-array "^1.1.11"
is-unicode-supported@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@ -4168,6 +4388,11 @@ keyv@^4.5.3:
dependencies:
json-buffer "3.0.1"
kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
labeled-stream-splicer@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-1.0.2.tgz#4615331537784981e8fd264e1f3a434c4e0ddd65"
@ -4252,6 +4477,19 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==
dependencies:
chalk "^4.1.0"
is-unicode-supported "^0.1.0"
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -4280,6 +4518,11 @@ magic-string@0.25.1:
dependencies:
sourcemap-codec "^1.4.1"
make-error@^1.1.1:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
map-obj@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
@ -4369,6 +4612,11 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@ -4391,7 +4639,7 @@ minimist@0.0.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566"
integrity sha512-rSJ0cdmCj3qmKdObcnMcWgPVOyaOWlazLhZAJW0s6G6lx1ZEuFkraWmEH5LTvX90btkfHPclQBjvjU7A/kYRFg==
minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6:
minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@ -4452,6 +4700,11 @@ multisplice@^1.0.0:
resolved "https://registry.yarnpkg.com/multisplice/-/multisplice-1.0.0.tgz#e74cf2948dcb51a6c317fc5e22980a652f7830e9"
integrity sha512-KU5tVjIdTGsMb92JlWwEZCGrvtI1ku9G9GuNbWdQT/Ici1ztFXX0L8lWpbbC3pISVMfBNL56wdqplHvva2XSlA==
mute-stream@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
@ -4576,6 +4829,13 @@ once@^1.3.0:
dependencies:
wrappy "1"
onetime@^5.1.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
dependencies:
mimic-fn "^2.1.0"
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@ -4600,11 +4860,31 @@ optionator@^0.9.3:
prelude-ls "^1.2.1"
type-check "^0.4.0"
ora@^5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18"
integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==
dependencies:
bl "^4.1.0"
chalk "^4.1.0"
cli-cursor "^3.1.0"
cli-spinners "^2.5.0"
is-interactive "^1.0.0"
is-unicode-supported "^0.1.0"
log-symbols "^4.1.0"
strip-ansi "^6.0.0"
wcwidth "^1.0.1"
os-browserify@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
outpipe@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2"
@ -4936,6 +5216,14 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
randexp@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.5.3.tgz#f31c2de3148b30bdeb84b7c3f59b0ebb9fec3738"
integrity sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==
dependencies:
drange "^1.0.2"
ret "^0.2.0"
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
version "2.1.0"
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
@ -5172,6 +5460,11 @@ remove-accents@0.4.2:
resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.4.2.tgz#0a43d3aaae1e80db919e07ae254b285d9e1c7bb5"
integrity sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==
repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==
requireindex@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef"
@ -5192,7 +5485,7 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve@^1.1.4, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.4.0:
resolve@^1.1.4, resolve@^1.1.7, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.22.2, resolve@^1.4.0:
version "1.22.6"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.6.tgz#dd209739eca3aef739c626fea1b4f3c506195362"
integrity sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==
@ -5218,6 +5511,19 @@ resp-modifier@^6.0.0:
debug "^2.2.0"
minimatch "^3.0.2"
restore-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
dependencies:
onetime "^5.1.0"
signal-exit "^3.0.2"
ret@^0.2.0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c"
integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
@ -5243,6 +5549,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@ -5250,6 +5561,13 @@ run-parallel@^1.1.9:
dependencies:
queue-microtask "^1.2.2"
rxjs@^7.5.5:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
safe-array-concat@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c"
@ -5314,7 +5632,7 @@ semver@^6.1.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.4, semver@^7.5.4:
semver@^7.3.4, semver@^7.3.8, semver@^7.5.4:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
@ -5410,6 +5728,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.2:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
@ -5614,6 +5937,15 @@ string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha512-Yptehjogou2xm4UJbxJ4CxgZx12HBfeystp0y3x7s4Dj32ltVVG1Gg8YhKjHZkHicuKpZX/ffilA8505VbUbpw==
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string.prototype.matchall@^4.0.8:
version "4.0.10"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100"
@ -5675,7 +6007,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^6.0.1:
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -5790,7 +6122,7 @@ through2@^4.0.2:
dependencies:
readable-stream "3"
"through@>=2.2.7 <3", through@~2.3.4:
"through@>=2.2.7 <3", through@^2.3.6, through@~2.3.4:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
@ -5814,6 +6146,13 @@ tiny-lr@^2.0.0:
object-assign "^4.1.0"
qs "^6.4.0"
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
dependencies:
os-tmpdir "~1.0.2"
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@ -5846,6 +6185,37 @@ ts-loader@^9.4.4:
micromatch "^4.0.0"
semver "^7.3.4"
ts-node@^10.9.1:
version "10.9.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
ts-patch@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/ts-patch/-/ts-patch-3.0.2.tgz#cbdf88e4dfb596e4dab8f2c8269361d33270a0ba"
integrity sha512-iTg8euqiNsNM1VDfOsVIsP0bM4kAVXU38n7TGQSkky7YQX/syh6sDPIRkvSS0HjT8ZOr0pq1h+5Le6jdB3hiJQ==
dependencies:
chalk "^4.1.2"
global-prefix "^3.0.0"
minimist "^1.2.8"
resolve "^1.22.2"
semver "^7.3.8"
strip-ansi "^6.0.1"
tsconfig@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-5.0.3.tgz#5f4278e701800967a8fc383fd19648878f2a6e3a"
@ -5868,6 +6238,11 @@ tsify@^5.0.4:
through2 "^2.0.0"
tsconfig "^5.0.3"
tslib@^2.1.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tty-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
@ -5892,6 +6267,11 @@ type-fest@^0.20.2:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.21.3:
version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
@ -5959,6 +6339,16 @@ typescript@^5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
typia@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typia/-/typia-5.1.6.tgz#ee380512ee737bd704ddb1e3ef792b0a16f61639"
integrity sha512-in/m6hhsoS4jDfztT/hMlWVS670+0BcQNR0AX/sVctqrY/VnVs8cNdJiFn8iZdQ/QvLqWaT/FW1WUuibn8prMw==
dependencies:
commander "^10.0.0"
comment-json "^4.2.3"
inquirer "^8.2.5"
randexp "^0.5.3"
umd@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.3.tgz#aa9fe653c42b9097678489c01000acb69f0b26cf"
@ -6074,6 +6464,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
validatem-as-array-of@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/validatem-as-array-of/-/validatem-as-array-of-0.0.1.tgz#08ea8f5bd813bdffa703095f406290095b8bfd5a"
@ -6107,6 +6502,13 @@ watchify@^4.0.0:
through2 "^4.0.2"
xtend "^4.0.2"
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==
dependencies:
defaults "^1.0.3"
websocket-driver@>=0.5.1:
version "0.7.4"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760"
@ -6171,6 +6573,13 @@ which-typed-array@^1.1.11, which-typed-array@^1.1.2, which-typed-array@^1.1.9:
gopd "^1.0.1"
has-tostringtag "^1.0.0"
which@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
dependencies:
isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@ -6190,6 +6599,15 @@ wouter@^2.8.0-alpha.2:
dependencies:
use-sync-external-store "^1.0.0"
wrap-ansi@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -6215,6 +6633,11 @@ yallist@^4.0.0:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"