From 637f188ebec71fe4b0b80bbab4592d4c269d7d93 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Tue, 17 Oct 2023 12:46:06 +0200
Subject: [feature] Allow import/export/creation of domain allows via admin
 panel (#2264)
* it's happening!
* aaa
* fix silly whoopsie
* it's working pa! it's working ma!
* model report parameters
* shuffle some more stuff around
* getting there
* oo hoo
* finish tidying up for now
* aaa
* fix use form submit errors
* peepee poo poo
* aaaaa
* ffff
* they see me typin', they hatin'
* boop
* aaa
* oooo
* typing typing tappa tappa
* almost done typing
* weee
* alright
* push it push it real good doo doo doo doo doo doo
* thingy no worky
* almost done
* mutation modifers not quite right
* hmm
* it works
* view blocks + allows nicely
* it works!
* typia install
* the old linterino
* linter plz
---
 .../settings/lib/query/admin/custom-emoji/index.ts | 307 +++++++++++++++++++++
 1 file changed, 307 insertions(+)
 create mode 100644 web/source/settings/lib/query/admin/custom-emoji/index.ts
(limited to 'web/source/settings/lib/query/admin/custom-emoji/index.ts')
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 .
+*/
+
+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({
+			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({
+			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 }]
+		}),
+
+		searchItemForEmoji: build.mutation({
+			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,
+};
-- 
cgit v1.2.3