diff options
Diffstat (limited to 'web/source/settings/lib/query/admin/custom-emoji/index.ts')
-rw-r--r-- | web/source/settings/lib/query/admin/custom-emoji/index.ts | 307 |
1 files changed, 307 insertions, 0 deletions
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, +}; |