/*
* Vencord, a modification for Discord's desktop app
* Copyright (c) 2022 Vendicated and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
import { migratePluginSettings, Settings } from "@api/settings";
import { CheckedTextInput } from "@components/CheckedTextInput";
import { Devs } from "@utils/constants";
import Logger from "@utils/Logger";
import { makeLazy } from "@utils/misc";
import { ModalContent, ModalHeader, ModalRoot, openModal } from "@utils/modal";
import definePlugin from "@utils/types";
import { findByCodeLazy, findByPropsLazy } from "@webpack";
import { Forms, GuildStore, Margins, Menu, PermissionStore, React, Toasts, Tooltip, UserStore } from "@webpack/common";
const MANAGE_EMOJIS_AND_STICKERS = 1n << 30n;
const GuildEmojiStore = findByPropsLazy("getGuilds", "getGuildEmoji");
const uploadEmoji = findByCodeLazy('"EMOJI_UPLOAD_START"', "GUILD_EMOJIS(");
function getGuildCandidates(isAnimated: boolean) {
const meId = UserStore.getCurrentUser().id;
return Object.values(GuildStore.getGuilds()).filter(g => {
const canCreate = g.ownerId === meId ||
BigInt(PermissionStore.getGuildPermissions({ id: g.id }) & MANAGE_EMOJIS_AND_STICKERS) === MANAGE_EMOJIS_AND_STICKERS;
if (!canCreate) return false;
const emojiSlots = g.getMaxEmojiSlots();
const { emojis } = GuildEmojiStore.getGuilds()[g.id];
let count = 0;
for (const emoji of emojis)
if (emoji.animated === isAnimated) count++;
return count < emojiSlots;
}).sort((a, b) => a.name.localeCompare(b.name));
}
async function doClone(guildId: string, id: string, name: string, isAnimated: boolean) {
const data = await fetch(`${location.protocol}//${window.GLOBAL_ENV.CDN_HOST}/emojis/${id}.${isAnimated ? "gif" : "png"}`)
.then(r => r.blob());
const reader = new FileReader();
reader.onload = () => {
uploadEmoji({
guildId,
name: name.split("~")[0],
image: reader.result
}).then(() => {
Toasts.show({
message: `Successfully cloned ${name}!`,
type: Toasts.Type.SUCCESS,
id: Toasts.genId()
});
}).catch((e: any) => {
new Logger("EmoteCloner").error("Failed to upload emoji", e);
Toasts.show({
message: "Oopsie something went wrong :( Check console!!!",
type: Toasts.Type.FAILURE,
id: Toasts.genId()
});
});
};
reader.readAsDataURL(data);
}
const getFontSize = (s: string) => {
// [18, 18, 16, 16, 14, 12, 10]
const sizes = [20, 20, 18, 18, 16, 14, 12];
return sizes[s.length] ?? 4;
};
const nameValidator = /^\w+$/i;
function CloneModal({ id, name: emojiName, isAnimated }: { id: string; name: string; isAnimated: boolean; }) {
const [isCloning, setIsCloning] = React.useState(false);
const [name, setName] = React.useState(emojiName);
const [x, invalidateMemo] = React.useReducer(x => x + 1, 0);
const guilds = React.useMemo(() => getGuildCandidates(isAnimated), [isAnimated, x]);
return (
<>
Custom Name
(v.length > 1 && v.length < 32 && nameValidator.test(v))
|| "Name must be between 2 and 32 characters and only contain alphanumeric characters"
}
/>
>
);
}
migratePluginSettings("EmoteCloner", "EmoteYoink");
export default definePlugin({
name: "EmoteCloner",
description: "Adds a Clone context menu item to emotes to clone them your own server",
authors: [Devs.Ven],
dependencies: ["MenuItemDeobfuscatorAPI"],
patches: [{
// Literally copy pasted from ReverseImageSearch lol
find: "open-native-link",
replacement: {
match: /id:"open-native-link".{0,200}\(\{href:(.{0,3}),.{0,200}\},"open-native-link"\)/,
replace: "$&,$self.makeMenu(arguments[2])"
},
},
// Also copy pasted from Reverse Image Search
{
// pass the target to the open link menu so we can grab its data
find: "REMOVE_ALL_REACTIONS_CONFIRM_BODY,",
predicate: makeLazy(() => !Settings.plugins.ReverseImageSearch.enabled),
noWarn: true,
replacement: {
match: /(?.).onHeightUpdate.{0,200}(.)=(.)=.\.url;.+?\(null!=\3\?\3:\2[^)]+/,
replace: "$&,$.target"
}
}],
makeMenu(htmlElement: HTMLImageElement) {
if (htmlElement?.dataset.type !== "emoji")
return null;
const { id } = htmlElement.dataset;
const name = htmlElement.alt.match(/:(.*)(?:~\d+)?:/)?.[1];
if (!name || !id)
return null;
const isAnimated = new URL(htmlElement.src).pathname.endsWith(".gif");
return
openModal(modalProps => (
Clone {name}
))
}
>
;
},
});