mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 00:52:26 -05:00 
			
		
		
		
	
		
			
	
	
		
			308 lines
		
	
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			308 lines
		
	
	
	
		
			8.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | /* | ||
|  | 	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, | ||
|  | }; |