diff --git a/README.md b/README.md
index 649d4e31..c2dca9fd 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,10 @@ Visit [1_INSTALLING.md](/docs/1_INSTALLING.md)
- - SlashCommandMentionOptions
- - TalkInReverse
+- Plugins by [happyendermangit](https://github.com/happyendermangit/)
+- - CopyEmojiAsFormattedString (added from vishnyanetchereshnya's [pull request](https://github.com/Vendicated/Vencord/pull/2266))
+- - QuestsCompleter (added from vishnyanetchereshnya's [pull request](https://github.com/Vendicated/Vencord/pull/2393))
+
- PurgeMessages (by [bhop](https://github.com/prettylittlelies))
- PlatformSpoofer (by [drag](https://github.com/dragdotpng))
- Timezones (by [mantikafasi](https://github.com/mantikafasi) & [ArjixWasTaken](https://github.com/ArjixWasTaken))
diff --git a/src/suncordplugins/questCompleter/index.tsx b/src/suncordplugins/questCompleter/index.tsx
new file mode 100644
index 00000000..8d232460
--- /dev/null
+++ b/src/suncordplugins/questCompleter/index.tsx
@@ -0,0 +1,197 @@
+/*
+ * Vencord, a modification for Discord's desktop app
+ * Copyright (c) 2023 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 { showNotification } from "@api/Notifications";
+import { Devs } from "@utils/constants";
+import { localStorage } from "@utils/localStorage";
+import definePlugin from "@utils/types";
+import { findByProps } from "@webpack";
+import { Text } from "@webpack/common";
+
+interface Stream {
+ streamType: string;
+ state: string;
+ ownerId: string;
+ guildId: string;
+ channelId: string;
+}
+
+function getLeftQuests() {
+ const QuestsStore = findByProps("getQuest");
+ // check if user still has incompleted quests
+ const quest = [...QuestsStore.quests.values()].find(quest => quest.userStatus?.enrolledAt && !quest?.userStatus?.completedAt && new Date(quest?.config?.expiresAt) >= new Date());
+ return quest;
+}
+
+let interval;
+let quest;
+let ImagesConfig = {};
+
+export default definePlugin({
+ name: "QuestCompleter",
+ description: "A plugin to complete quests without having the game.",
+ authors: [Devs.HAPPY_ENDERMAN, Devs.SerStars],
+ patches: [
+ {
+ find: "\"invite-button\"",
+ replacement: {
+ match: /(function .+?\(.+?\){let{inPopout:.+allowIdle.+?}=.+?\.usePreventIdle\)\("popup"\),(.+?)=\[\];if\(.+?\){.+"chat-spacer"\)\)\),\(\d,.+?\.jsx\)\(.+?,{children:).+?}}/,
+ replace: "$1[$self.renderQuestButton(),...$2]})}}"
+ }
+ }
+ ],
+ settingsAboutComponent() {
+ const isDesktop = navigator.userAgent.includes("discord/");
+ const hasQuestsExtensionEnabled = localStorage.getItem("QUESTS_EXT_ENABLED");
+
+ return (<>
+ {
+ isDesktop || hasQuestsExtensionEnabled ?
+
+ The plugin should work properly because you {isDesktop ? "are on the Desktop Client." : "installed our extension."}
+
+ :
+
+ This plugin won't work right now. Please download
+ our extension
+ to make the plugin work on web.
+
+ }
+ >);
+ },
+ start() {
+ const currentUserId: string = findByProps("getCurrentUser").getCurrentUser().id;
+ window.currentUserId = currentUserId; // this is here because discord will lag if we get the current user id every time
+ },
+ renderQuestButton() {
+ const currentStream: Stream | null = findByProps("getCurrentUserActiveStream").getCurrentUserActiveStream();
+ let shouldDisable = !!interval;
+ const { Divider } = findByProps("Divider", "Icon");
+
+ if (!currentStream) {
+ shouldDisable = true;
+ }
+ if (currentStream) {
+ if (!findByProps("getParticipants").getParticipants(currentStream.channelId).filter(participent => participent.user.id !== window.currentUserId).length) {
+ shouldDisable = true;
+ }
+ if (currentStream?.ownerId !== window.currentUserId) {
+ shouldDisable = true;
+ }
+ }
+ if (!getLeftQuests()) {
+ shouldDisable = true;
+ }
+
+
+
+ const ToolTipButton = findByProps("CenterControlButton").default;
+ const QuestsIcon = () => props => (
+
+
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+ },
+ openCompleteQuestUI() {
+ // check if user is sharing screen and there is someone that is watching the stream
+
+ const currentStream: Stream | null = findByProps("getCurrentUserActiveStream").getCurrentUserActiveStream();
+ const encodedStreamKey = findByProps("encodeStreamKey").encodeStreamKey(currentStream);
+ quest = getLeftQuests();
+ ImagesConfig = {
+ icon: findByProps("getQuestBarHeroAssetUrl").getQuestBarHeroAssetUrl(quest),
+ image: findByProps("getHeroAssetUrl").getHeroAssetUrl(quest)
+ };
+
+ const heartBeat = async () => {
+ findByProps("HTTP", "getAPIBaseURL"); // rest api module
+ findByProps("sendHeartbeat").sendHeartbeat({ questId: quest.id, streamKey: encodedStreamKey });
+ };
+
+ heartBeat();
+ interval = setInterval(heartBeat, 60500); // send the heartbeat each minute
+
+ return;
+ },
+ flux: {
+ STREAM_STOP: event => {
+ const stream: Stream = findByProps("encodeStreamKey").decodeStreamKey(event.streamKey);
+ // we check if the stream is by the current user id so we do not clear the interval without any reason.
+ if (stream.ownerId === window.currentUserId && interval) {
+ clearInterval(interval);
+ interval = null;
+ }
+ },
+ QUESTS_SEND_HEARTBEAT_FAILURE: () => {
+ showNotification(
+ {
+ title: "Couldn't start Completing Quest",
+ body: "You are probally using web, please check the plugin settings for help.",
+ ...ImagesConfig
+ }
+ );
+ clearInterval(interval);
+ interval = null;
+ },
+ QUESTS_SEND_HEARTBEAT_SUCCESS: event => {
+
+ const a = event.userStatus.streamProgressSeconds * 100;
+ const b = quest.config.streamDurationRequirementMinutes * 60;
+ showNotification({
+ title: `${quest.config.applicationName} - Quests Completer`,
+ body: `Current progress: ${Math.floor(a / b)}% (${Math.floor(event.userStatus.streamProgressSeconds / 60)} minutes.)`,
+ ...ImagesConfig
+ });
+
+ if (event.userStatus.streamProgressSeconds === quest.config.streamDurationRequirementMinutes * 60) {
+ showNotification({
+ title: `${quest.config.applicationName} - Quests Completer`,
+ body: "Quest Completed",
+ ...ImagesConfig
+ });
+ clearInterval(interval);
+ interval = null;
+ }
+ }
+ }
+});
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index e621a03d..a63d9e81 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -530,6 +530,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({
name: "DaBluLite",
id: 582170007505731594n,
},
+ SerStars: {
+ name: "SerStars",
+ id: 861631850681729045n,
+ },
} satisfies Record);
export const SuncordDevs = /* #__PURE__*/ Object.freeze({