summaryrefslogtreecommitdiff
path: root/web/source/settings/lib/query/admin/custom-emoji/index.ts
diff options
context:
space:
mode:
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.ts307
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,
+};