diff options
Diffstat (limited to 'web/source/settings/lib/query/admin')
-rw-r--r-- | web/source/settings/lib/query/admin/custom-emoji.js | 195 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/import-export.js | 212 | ||||
-rw-r--r-- | web/source/settings/lib/query/admin/index.js | 84 |
3 files changed, 491 insertions, 0 deletions
diff --git a/web/source/settings/lib/query/admin/custom-emoji.js b/web/source/settings/lib/query/admin/custom-emoji.js new file mode 100644 index 000000000..94917f382 --- /dev/null +++ b/web/source/settings/lib/query/admin/custom-emoji.js @@ -0,0 +1,195 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); + +const { unwrapRes } = require("../lib"); + +module.exports = (build) => ({ + getAllEmoji: build.query({ + query: (params = {}) => ({ + url: "/api/v1/admin/custom_emojis", + params: { + limit: 0, + ...params + } + }), + providesTags: (res) => + res + ? [...res.map((emoji) => ({ type: "Emojis", id: emoji.id })), { type: "Emojis", id: "LIST" }] + : [{ type: "Emojis", id: "LIST" }] + }), + + getEmoji: build.query({ + query: (id) => ({ + url: `/api/v1/admin/custom_emojis/${id}` + }), + providesTags: (res, error, id) => [{ type: "Emojis", 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: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }] + : [{ type: "Emojis", 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: "Emojis", id: "LIST" }, { type: "Emojis", id: res.id }] + : [{ type: "Emojis", id: "LIST" }] + }), + + deleteEmoji: build.mutation({ + query: (id) => ({ + method: "DELETE", + url: `/api/v1/admin/custom_emojis/${id}` + }), + invalidatesTags: (res, error, id) => [{ type: "Emojis", 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.shortcode, 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: "Emojis", 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/import-export.js b/web/source/settings/lib/query/admin/import-export.js new file mode 100644 index 000000000..94e462bd2 --- /dev/null +++ b/web/source/settings/lib/query/admin/import-export.js @@ -0,0 +1,212 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const Promise = require("bluebird"); +const isValidDomain = require("is-valid-domain"); +const fileDownload = require("js-file-download"); + +const { + replaceCacheOnMutation, + domainListToObject, + unwrapRes +} = require("../lib"); + +function parseDomainList(list) { + if (list[0] == "[") { + return JSON.parse(list); + } 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) => { + entry.valid = (entry.valid !== false) && isValidDomain(entry.domain, { wildcard: true, allowUnicode: true }); + 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) => { + return { data }; + }).catch((e) => { + return { error: e.toString() }; + }); + } + }), + exportDomainList: build.mutation({ + queryFn: (formData, api, _extraOpts, baseQuery) => { + return Promise.try(() => { + return baseQuery({ + url: `/api/v1/admin/domain_blocks` + }); + }).then(unwrapRes).then((blockedInstances) => { + return blockedInstances.map((entry) => { + if (formData.exportType == "json") { + return { + domain: entry.domain, + public_comment: entry.public_comment + }; + } else { + return entry.domain; + } + }); + }).then((exportList) => { + if (formData.exportType == "json") { + return JSON.stringify(exportList); + } else { + return exportList.join("\n"); + } + }).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 mime; + + let filename = [ + domain, + "blocklist", + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, "0"), + date.getDate().toString().padStart(2, "0"), + ].join("-"); + + if (formData.exportType == "json") { + filename += ".json"; + mime = "application/json"; + } else { + filename += ".txt"; + mime = "text/plain"; + } + + fileDownload(exportAsString, filename, 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") + }) +}); + +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 (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 new file mode 100644 index 000000000..33e14521e --- /dev/null +++ b/web/source/settings/lib/query/admin/index.js @@ -0,0 +1,84 @@ +/* + GoToSocial + Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org + + 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/>. +*/ + +"use strict"; + +const { + replaceCacheOnMutation, + removeFromCacheOnMutation, + domainListToObject +} = require("../lib"); +const base = require("../base"); + +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 + } + }) + }), + 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; + } + }) + }), + ...require("./import-export")(build), + ...require("./custom-emoji")(build) +}); + +module.exports = base.injectEndpoints({ endpoints });
\ No newline at end of file |