summaryrefslogtreecommitdiff
path: root/web/source/settings/lib/query
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-10-17 12:46:06 +0200
committerLibravatar GitHub <noreply@github.com>2023-10-17 12:46:06 +0200
commit637f188ebec71fe4b0b80bbab4592d4c269d7d93 (patch)
tree6e1136dee4d854af021e0a571a67038d32083e4b /web/source/settings/lib/query
parent[chore]: Bump github.com/microcosm-cc/bluemonday from 1.0.25 to 1.0.26 (#2266) (diff)
downloadgotosocial-637f188ebec71fe4b0b80bbab4592d4c269d7d93.tar.xz
[feature] Allow import/export/creation of domain allows via admin panel (#2264)v0.12.0-rc1
* 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
Diffstat (limited to 'web/source/settings/lib/query')
-rw-r--r--web/source/settings/lib/query/admin/custom-emoji.js194
-rw-r--r--web/source/settings/lib/query/admin/custom-emoji/index.ts307
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/export.ts155
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/get.ts56
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/import.ts140
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/process.ts163
-rw-r--r--web/source/settings/lib/query/admin/domain-permissions/update.ts109
-rw-r--r--web/source/settings/lib/query/admin/import-export.js264
-rw-r--r--web/source/settings/lib/query/admin/index.js165
-rw-r--r--web/source/settings/lib/query/admin/index.ts148
-rw-r--r--web/source/settings/lib/query/admin/reports.js51
-rw-r--r--web/source/settings/lib/query/admin/reports/index.ts83
-rw-r--r--web/source/settings/lib/query/gts-api.ts16
-rw-r--r--web/source/settings/lib/query/lib.js81
-rw-r--r--web/source/settings/lib/query/oauth/index.ts10
-rw-r--r--web/source/settings/lib/query/query-modifiers.ts150
-rw-r--r--web/source/settings/lib/query/transforms.ts78
-rw-r--r--web/source/settings/lib/query/user/index.ts8
18 files changed, 1410 insertions, 768 deletions
diff --git a/web/source/settings/lib/query/admin/custom-emoji.js b/web/source/settings/lib/query/admin/custom-emoji.js
deleted file mode 100644
index 6e7c772a2..000000000
--- a/web/source/settings/lib/query/admin/custom-emoji.js
+++ /dev/null
@@ -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
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/custom-emoji/index.ts b/web/source/settings/lib/query/admin/custom-emoji/index.ts
new file mode 100644
index 000000000..d624b0580
--- /dev/null
+++ b/web/source/settings/lib/query/admin/custom-emoji/index.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/export.ts b/web/source/settings/lib/query/admin/domain-permissions/export.ts
new file mode 100644
index 000000000..b6ef560f4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/export.ts
@@ -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 };
diff --git a/web/source/settings/lib/query/admin/domain-permissions/get.ts b/web/source/settings/lib/query/admin/domain-permissions/get.ts
new file mode 100644
index 000000000..3e27742d4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/get.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/import.ts b/web/source/settings/lib/query/admin/domain-permissions/import.ts
new file mode 100644
index 000000000..dde488625
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/import.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/admin/domain-permissions/process.ts b/web/source/settings/lib/query/admin/domain-permissions/process.ts
new file mode 100644
index 000000000..017d02bb4
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/process.ts
@@ -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 };
diff --git a/web/source/settings/lib/query/admin/domain-permissions/update.ts b/web/source/settings/lib/query/admin/domain-permissions/update.ts
new file mode 100644
index 000000000..a6b4b2039
--- /dev/null
+++ b/web/source/settings/lib/query/admin/domain-permissions/update.ts
@@ -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
+};
diff --git a/web/source/settings/lib/query/admin/import-export.js b/web/source/settings/lib/query/admin/import-export.js
deleted file mode 100644
index 9a04438c2..000000000
--- a/web/source/settings/lib/query/admin/import-export.js
+++ /dev/null
@@ -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];
- }
- });
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/index.js b/web/source/settings/lib/query/admin/index.js
deleted file mode 100644
index 7a55389d3..000000000
--- a/web/source/settings/lib/query/admin/index.js
+++ /dev/null
@@ -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 }); \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts
new file mode 100644
index 000000000..e61179216
--- /dev/null
+++ b/web/source/settings/lib/query/admin/index.ts
@@ -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;
diff --git a/web/source/settings/lib/query/admin/reports.js b/web/source/settings/lib/query/admin/reports.js
deleted file mode 100644
index 1c45bb7bc..000000000
--- a/web/source/settings/lib/query/admin/reports.js
+++ /dev/null
@@ -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" }]
- })
-}); \ No newline at end of file
diff --git a/web/source/settings/lib/query/admin/reports/index.ts b/web/source/settings/lib/query/admin/reports/index.ts
new file mode 100644
index 000000000..253e8238c
--- /dev/null
+++ b/web/source/settings/lib/query/admin/reports/index.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts
index 9e043137c..a07f5ff1e 100644
--- a/web/source/settings/lib/query/gts-api.ts
+++ b/web/source/settings/lib/query/gts-api.ts
@@ -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 };
diff --git a/web/source/settings/lib/query/lib.js b/web/source/settings/lib/query/lib.js
deleted file mode 100644
index 1025ca3a7..000000000
--- a/web/source/settings/lib/query/lib.js
+++ /dev/null
@@ -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 });
- }));
- });
- }
- };
- };
-} \ No newline at end of file
diff --git a/web/source/settings/lib/query/oauth/index.ts b/web/source/settings/lib/query/oauth/index.ts
index 9af2dd5fb..f62a29596 100644
--- a/web/source/settings/lib/query/oauth/index.ts
+++ b/web/source/settings/lib/query/oauth/index.ts
@@ -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; \ No newline at end of file
+} = extended;
diff --git a/web/source/settings/lib/query/query-modifiers.ts b/web/source/settings/lib/query/query-modifiers.ts
new file mode 100644
index 000000000..d6bf0b6ae
--- /dev/null
+++ b/web/source/settings/lib/query/query-modifiers.ts
@@ -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,
+};
diff --git a/web/source/settings/lib/query/transforms.ts b/web/source/settings/lib/query/transforms.ts
new file mode 100644
index 000000000..d915e0b13
--- /dev/null
+++ b/web/source/settings/lib/query/transforms.ts
@@ -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);
+ };
+}
diff --git a/web/source/settings/lib/query/user/index.ts b/web/source/settings/lib/query/user/index.ts
index 751e38e5b..a7cdad2fd 100644
--- a/web/source/settings/lib/query/user/index.ts
+++ b/web/source/settings/lib/query/user/index.ts
@@ -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`,