Add files via upload

This commit is contained in:
OTKUSteyler 2026-09-03 17:15:26 +02:00 • committed by GitHub
parent d26de6be8e
commit cbcbd53cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 651 additions and 0 deletions

View file

@ -0,0 +1,13 @@
import { Forms } from "@vendetta/ui/components";
const { FormSection, FormText } = Forms;
export default () => (
<FormSection title="FavouriteAnything">
<FormText>
Adds the favourite button to ALL media in the image viewer, not just GIFs!
</FormText>
<FormText style={{ marginTop: 8, color: "#b9bbbe" }}>
Images and videos can now be saved to your GIF favourites picker. Works by patching Discord's GIFFavButton to accept all media types.
</FormText>
</FormSection>
)

View file

@ -0,0 +1,169 @@
// src/core/plugins/favouriteanything/index.ts
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { before } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
type PatchCleanupFn = () => void;
let origType: Function | null = null;
let memoWrapper: any = null;
let retryTimeout: ReturnType<typeof setTimeout> | null = null;
let unpatchAddFavorite: PatchCleanupFn | null = null;
let origUseFavoriteGIFsMobile: Function | null = null;
let favMobileModule: any = null;
const VIDEO_EXT = [".mp4", ".webm", ".mov", ".avi", ".mkv", ".flv", ".wmv", ".m4v", ".gifv"];
const processed = new WeakSet<object>();
function isVideo(url: string): boolean {
if (!url) return false;
const path = url.split("?")[0].toLowerCase();
return VIDEO_EXT.some(e => path.endsWith(e));
}
function makeVideoThumbnail(url: string): string {
if (!url) return url;
let out = url.replace("cdn.discordapp.com", "media.discordapp.net");
if (out.includes("media.discordapp.net") || out.includes("images-ext")) {
return out + (out.includes("?") ? "&" : "?") + "format=jpeg";
}
return url;
}
function findGIFFavButton(): any {
const modules = (globalThis as any).__bunny?.metro?.modules ?? (globalThis as any).modules;
if (!modules) return null;
for (const id in modules) {
try {
const mod = modules[id]?.publicModule?.exports;
if (!mod) continue;
const def = mod.default;
if (def?.$$typeof?.toString().includes("memo") && def.type) {
if (def.type.displayName === "GIFFavButton" || def.type.name === "GIFFavButton") return def;
}
if (typeof mod === "function" && (mod.displayName === "GIFFavButton" || mod.name === "GIFFavButton")) return mod;
} catch {}
}
return null;
}
function patchSource(source: any): any {
if (source.isGIFV) return source;
return {
...source,
isGIFV: true,
embedURI: source.embedURI || source.sourceURI || source.uri,
videoURI: source.videoURI || source.uri,
embedProviderName: source.embedProviderName || "",
};
}
function applyPatch(): boolean {
memoWrapper = findGIFFavButton();
if (!memoWrapper) return false;
origType = memoWrapper.type;
memoWrapper.type = function PatchedGIFFavButton(props: any) {
const p = { ...props };
if (p.source && !p.source.isGIFV) p.source = patchSource(p.source);
return (origType as Function)(p);
};
(memoWrapper.type as any).displayName = "GIFFavButton";
return true;
}
function patchAddFavorite(): void {
const favModule = findByProps("addFavoriteGIF");
if (!favModule) return;
unpatchAddFavorite = before("addFavoriteGIF", favModule, (args: any[]) => {
const data = args[0];
if (!data || typeof data !== "object") return;
const url = data.url || "";
const src = data.src || "";
if (isVideo(url) || isVideo(src)) {
data.format = 2;
} else if (data.format === 2) {
data.format = 1;
}
});
}
function patchMobileFavorites(): void {
const mod = findByProps("useFavoriteGIFsMobile");
if (!mod) return;
favMobileModule = mod;
origUseFavoriteGIFsMobile = mod.useFavoriteGIFsMobile;
let lastFavs: any = null;
mod.useFavoriteGIFsMobile = function (...args: any[]) {
const result = (origUseFavoriteGIFsMobile as Function).apply(this, args);
if (result?.favorites && Array.isArray(result.favorites) && result.favorites !== lastFavs) {
lastFavs = result.favorites;
for (const item of result.favorites) {
if (!item || processed.has(item)) continue;
processed.add(item);
if (isVideo(item.url) || isVideo(item.src)) {
item.src = makeVideoThumbnail(item.src || item.url);
}
}
}
return result;
};
}
export default defineCorePlugin({
manifest: {
id: "bunny.favouriteanything",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "FavouriteAnything",
description: "Adds the favourite button to ALL media, not just GIFs",
authors: [{ name: "TheUnrealZaka" }],
},
},
start() {
if (applyPatch()) {
logger.log("[FavouriteAnything] Patched GIFFavButton.");
} else {
let retries = 0;
const tryPatch = () => {
if (applyPatch()) {
logger.log("[FavouriteAnything] Patched GIFFavButton (after retry).");
retryTimeout = null;
} else if (retries++ < 50) {
retryTimeout = setTimeout(tryPatch, 300);
} else {
logger.error("[FavouriteAnything] GIFFavButton not found after retries.");
}
};
retryTimeout = setTimeout(tryPatch, 300);
}
patchAddFavorite();
patchMobileFavorites();
},
stop() {
if (retryTimeout) { clearTimeout(retryTimeout); retryTimeout = null; }
if (memoWrapper && origType) {
memoWrapper.type = origType;
origType = null;
memoWrapper = null;
}
if (unpatchAddFavorite) { unpatchAddFavorite(); unpatchAddFavorite = null; }
if (favMobileModule && origUseFavoriteGIFsMobile) {
favMobileModule.useFavoriteGIFsMobile = origUseFavoriteGIFsMobile;
origUseFavoriteGIFsMobile = null;
favMobileModule = null;
}
},
});

Binary file not shown.

View file

@ -0,0 +1,145 @@
import { defineCorePlugin } from "..";
import { findByProps } from "@metro";
import { logger } from "@lib/utils/logger";
const { ReactNative } = window as any;
const { DCDSoundManager } = ReactNative?.NativeModules || {};
const AUDIO_URL = "https://github.com/OTKUSteyler/GoonCord/raw/refs/heads/main/src/core/plugins/maxverstappen/Max%20Verstappen.mp3";
const SOUND_ID = 6972;
const TRIGGER_CHANCE = 0.33;
const MAX_PATTERN = /\bmax\s*verst[ae]pp?en\b/i;
let isPlaying = false;
let isPrepared = false;
let isPreparing = false;
let soundDuration = 30000;
let timeoutId: NodeJS.Timeout | null = null;
let onMessageCreate: ((e: any) => void) | null = null;
let onMessageSend: ((e: any) => void) | null = null;
function shouldTrigger(): boolean {
return Math.random() < TRIGGER_CHANCE;
}
function checkText(text: string): boolean {
if (!text) return false;
return MAX_PATTERN.test(text) && shouldTrigger();
}
function ensurePrepared(): Promise<number> {
return new Promise((resolve) => {
if (isPrepared) return resolve(soundDuration);
if (isPreparing) return resolve(-1);
isPreparing = true;
DCDSoundManager.prepare(AUDIO_URL, "music", SOUND_ID, (error: any, sound: any) => {
isPreparing = false;
if (error) {
logger.error("[MaxVerstappen] Failed to prepare:", error);
return resolve(-1);
}
isPrepared = true;
soundDuration = sound?.duration || 30000;
resolve(soundDuration);
});
});
}
async function playAudio() {
if (!DCDSoundManager) return;
if (isPlaying) return;
const duration = await ensurePrepared();
if (duration === -1) return;
isPlaying = true;
try {
await DCDSoundManager.play(SOUND_ID);
timeoutId = setTimeout(() => {
DCDSoundManager.stop(SOUND_ID);
isPlaying = false;
timeoutId = null;
}, soundDuration);
} catch (e) {
logger.error("[MaxVerstappen] Playback error:", e);
isPlaying = false;
}
}
export default defineCorePlugin({
manifest: {
id: "bunny.maxverstappen",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "Max Verstappen",
description: "Plays audio when Max Verstappen is mentioned (33% chance)",
authors: [{ name: "GoonCord Team" }],
},
},
start() {
if (!DCDSoundManager) {
logger.error("[MaxVerstappen] DCDSoundManager not found");
return;
}
const FluxDispatcher = findByProps("dispatch", "subscribe");
if (!FluxDispatcher) {
logger.error("[MaxVerstappen] FluxDispatcher not found");
return;
}
onMessageCreate = ({ message }: any) => {
try {
if (!message?.content) return;
if (checkText(message.content)) playAudio();
} catch (e) {
logger.error("[MaxVerstappen] MESSAGE_CREATE error:", e);
}
};
onMessageSend = ({ message }: any) => {
try {
if (!message?.content) return;
if (checkText(message.content)) playAudio();
} catch (e) {
logger.error("[MaxVerstappen] MESSAGE_SEND error:", e);
}
};
FluxDispatcher.subscribe("MESSAGE_CREATE", onMessageCreate);
FluxDispatcher.subscribe("MESSAGE_SEND", onMessageSend);
logger.log("[MaxVerstappen] Enabled");
},
stop() {
if (DCDSoundManager && isPlaying) {
DCDSoundManager.stop(SOUND_ID);
isPlaying = false;
}
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
const FluxDispatcher = findByProps("dispatch", "subscribe");
if (FluxDispatcher) {
if (onMessageCreate) FluxDispatcher.unsubscribe("MESSAGE_CREATE", onMessageCreate);
if (onMessageSend) FluxDispatcher.unsubscribe("MESSAGE_SEND", onMessageSend);
}
onMessageCreate = null;
onMessageSend = null;
isPrepared = false;
isPreparing = false;
soundDuration = 30000;
logger.log("[MaxVerstappen] Disabled");
},
});

View file

@ -0,0 +1,76 @@
// src/core/plugins/NoDelete/index.ts
import { defineCorePlugin } from "..";
import { findByStoreName } from "@metro";
import { before } from "@lib/api/patcher";
import { logger } from "@lib/utils/logger";
import { FluxDispatcher } from "@metro/common";
type PatchCleanupFn = () => void;
let patches: PatchCleanupFn[] = [];
let MessageStore: any = null;
let deleteable: string[] = [];
function patchDispatcher(): PatchCleanupFn {
return before("dispatch", FluxDispatcher, (args: any[]) => {
try {
if (!MessageStore) MessageStore = findByStoreName("MessageStore");
const event = args[0];
if (!event || event?.type !== "MESSAGE_DELETE") return;
if (!event?.id || !event?.channelId) return;
if (deleteable.includes(event.id)) {
deleteable.splice(deleteable.indexOf(event.id), 1);
return;
}
deleteable.push(event.id);
args[0] = {
type: "MESSAGE_EDIT_FAILED_AUTOMOD",
messageData: {
type: 1,
message: {
channelId: event.channelId,
messageId: event.id,
},
},
errorResponseBody: {
code: 200000,
message: "[deleted]",
},
};
} catch (e) {
logger.error("NoDelete: dispatcher patch failed", e);
}
});
}
export default defineCorePlugin({
manifest: {
id: "bunny.nodelete",
version: "1.0.0",
type: "plugin",
spec: 3,
main: "",
display: {
name: "NoDelete",
description: "Prevents messages from being deleted from your view",
authors: [{ name: "meqativ" }],
},
},
start() {
patches = [patchDispatcher()].filter(Boolean);
logger.log("NoDelete: Enabled");
},
stop() {
patches.forEach((p) => p?.());
patches = [];
deleteable = [];
MessageStore = null;
logger.log("NoDelete: Disabled");
},
});

View file

@ -0,0 +1,90 @@
import { React, ReactNative } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage";
import { showConfirmationAlert } from "@vendetta/ui/alerts";
import { Forms } from "@vendetta/ui/components";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { getTranslation } from "./translations.js";
import ItemWithRemove from "../../common/ui/ItemWithRemove.jsx";
import { findByStoreName, findByProps } from "@vendetta/metro";
let UserStore, UncachedUserManager, Profiles;
export default (props) => {
UserStore ??= findByStoreName("UserStore");
UncachedUserManager ??= findByProps("fetchProfile", "getUser", "setFlag");
Profiles ??= findByProps("showUserProfile");
async function openProfile(userId) {
const show = Profiles.showUserProfile;
UserStore.getUser(userId) ? show({ userId }) : UncachedUserManager.getUser(userId).then(({ id }) => show({ userId: id }));
}
useProxy(storage);
const [users, setUsers] = React.useState(storage["ignore"]["users"]);
const handleRemoveUser = (userId) => {
const newArr = users.filter((id) => id !== userId);
storage["ignore"].users = newArr;
setUsers(newArr);
};
const handleClearUsers = () => {
storage["ignore"].users = [];
setUsers([]);
};
let uncached = 0;
return (
<ReactNative.ScrollView style={{ flex: 1 }}>
<Forms.FormSection title={getTranslation("settings.titles.settings")} titleStyleType="no_border">
<Forms.FormRow label={getTranslation("settings.showTimestamps")} trailing={<Forms.FormSwitch value={storage.timestamps} onValueChange={(v) => (storage.timestamps = v)} />} />
<Forms.FormRow label={getTranslation("settings.ewTimestampFormat")} trailing={<Forms.FormSwitch value={storage["ew"]} onValueChange={(v) => (storage.ew = v)} />} />
<Forms.FormDivider />
<Forms.FormRow label={getTranslation("settings.youDeletedItWarning")} />
</Forms.FormSection>
<Forms.FormSection title={getTranslation("settings.titles.filters")}>
<Forms.FormRow label={getTranslation("settings.ignoreBots")} trailing={<Forms.FormSwitch value={storage["ignore"].bots} onValueChange={(value) => (storage["ignore"].bots = value)} />} />
<Forms.FormRow
label={getTranslation("settings.clearUsersLabel", true)?.make?.(users.length)}
trailing={<Forms.FormRow.Icon source={getAssetIDByName("ic_trash_24px")} />}
onPress={() => {
if (users.length !== 0)
showConfirmationAlert({
title: getTranslation("settings.confirmClear.title"),
content: getTranslation("settings.confirmClear.description", true)?.make?.(users.length),
confirmText: getTranslation("settings.confirmClear.yes"),
cancelText: getTranslation("settings.confirmClear.no"),
confirmColor: "brand",
onConfirm: handleClearUsers,
});
}}
/>
<ReactNative.ScrollView style={{ flex: 1, gap: 3, marginLeft: 15 }}>
{users.map((id) => {
const User = UserStore.getUser(id) ?? {};
let pfp = User?.getAvatarURL?.(null,26)?.replace?.(/\.(gif|webp)/, ".png");
if (!pfp) {
pfp = "https://cdn.discordapp.com/embed/avatars/1.png?size=48";
User.username = `${id} Uncached`;
User.discriminator = "0";
if (uncached === 0) User.username += ", press the avatar";
uncached++;
}
return (
<ItemWithRemove
imageSource={{ uri: pfp }}
onImagePress={() => {
openProfile(id);
}}
onRemove={() => handleRemoveUser(id)}
label={User.username + (User.discriminator == 0 ? "" : `#${User.discriminator}`)}
labelRemove={getTranslation("settings.removeUserButton")}
/>
);
})}
</ReactNative.ScrollView>
<Forms.FormDivider />
<Forms.FormRow label={getTranslation("settings.addUsersInfo")} />
</Forms.FormSection>
</ReactNative.ScrollView>
);
};

View file

@ -0,0 +1,158 @@
function slavicNoun(number, $1, $2, $5) {
let n = Math.abs(number);
n %= 100;
if (n >= 5 && n <= 20) return $5;
n %= 10;
if (n === 1) return $1;
if (n >= 2 && n <= 4) return $2;
return $5;
}
function latinNoun(number, $1, $x) {
return number === 1 ? $1 : $x;
}
export let massive = {};
massive = {
settings: {
titles: {
settings: {
"en-GB": "Settings",
uk: "Налаштування",
ru: "Настройки",
},
filters: {
"en-GB": "Filters",
uk: "Фільтри",
ru: "Фильтры",
},
},
showTimestamps: {
"en-GB": "Show the time of deletion",
uk: "Показувати час видалення",
ru: "Показывать время удаления",
},
ewTimestampFormat: {
"en-GB": "Use 12-hour format",
uk: "Використовувати 12-годинний формат",
ru: "Использовать 12-часовой формат",
},
youDeletedItWarning: {
"en-GB": "The messages YOU deleted - are not saved",
uk: "Повідомлення які видалили ВИ - не зберігаются",
ru: "Сообщения удаленные ВАМИ - не сохраняются",
},
addUsersInfo: {
"en-GB": () =>
`To add or remove users from the ignore list, follow these steps:\n` +
`1. open their profile\n` +
`2. press the •••\n` +
`3. press "${massive.optionLabels[0]["en-GB"]}"\n` +
`4. 🎉`,
uk: () =>
`Щоб добавити когось до списку ігнорованих користувачів, виконайте ці дії:\n` +
`1. відкрите їх профіль\n` +
`2. натисніть •••\n` +
`3. натисніть "${massive.optionLabels[0]["uk"]}"\n` +
`4. 🎉`,
ru: () =>
`Чтобы добавить кого-то в список игнорированных пользователей - следуйте этим шагам\n` +
`1. откройте их профиль\n` +
`2. нажмите •••\n` +
`3. нажмите "${massive.optionLabels[0]["ru"]}"\n` +
`4. 🎉`,
},
ignoreBots: {
"en-GB": "Ignore bots",
uk: "Ігнорувати ботів",
ru: "Игнорировать ботов",
},
clearUsersLabel: {
"en-GB": (amount) =>
`You have ${amount} user${latinNoun(amount, "", "s")} in the ignored users list`,
uk: (amount) =>
`Ви маєте ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} у списку ігнорованих користувачів`,
ru: (amount) =>
`Вы имеете ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} в списке игнорированных пользователей`,
},
confirmClear: {
title: {
"en-GB": "Hold on!",
uk: "Почекай-но!",
ru: "Положди-ка!",
},
description: {
"en-GB": (amount) =>
`This will remove ${amount} user${latinNoun(amount, "", "s")} from the ignored users list.\nDo you you really want to do that?`,
uk: (amount) =>
`Це прибере ${amount} користувач${slavicNoun(amount, "а", "а", "ів")} зі списку ігнорованих користувачів.\nВи дійсно хочете продовжити?`,
ru: (amount) =>
`Это уберет ${amount} пользовател${slavicNoun(amount, "я", "я", "ей")} из списка игнорированных пользователей.\nВы действительно хотите продолжить?`,
},
yes: {
"en-GB": "Yes",
uk: "Так",
ru: "Да",
},
no: {
"en-GB": "Cancel",
uk: "Відминити",
ru: "Отменить",
},
},
removeUserButton: {
"en-GB": "REMOVE",
uk: "ПРИБРАТИ",
ru: "УБРАТЬ",
},
},
optionLabels: [
{
"en-GB": "Add to NoDelete ignored users list",
uk: "Добавити до списку ігнорованих користувачів у NoDelete",
ru: "Добавить в список игнорированных пользователей в NoDelete",
},
{
"en-GB": "Remove from the NoDelete ignore list",
uk: "Прибрати з списку ігнорованих користувачів у NoDelete",
ru: "Убрать из списка игнорированных пользователей в NoDelete",
},
],
toastLabels: [
{
"en-GB": "Added {user} to the ignored users list",
uk: "{user} добавлено до списку ігнорованих користувачів",
ru: "{user} добавлены в список игнорированных пользователей",
},
{
"en-GB": "Removed {user} from the ignored users list",
uk: "{user} прибрано зі списку ігнорованих користувачів",
ru: "{user} убраны из списка игнорированных пользователей",
},
],
thisMessageWasDeleted: {
"en-GB": "This message was deleted",
uk: "Це повідомлення було видалено",
ru: "Это сообщение было удалено",
},
};
export const locale = () =>
vendetta.metro.findByStoreName("LocaleStore").locale;
const defaultLocale = "en-GB";
export function getTranslation(find, fn) {
let value = massive;
for (const key of find.split(".")) {
// biome-ignore lint/suspicious/noPrototypeBuiltins: meow
if (value?.hasOwnProperty(key)) {
value = value[key];
} else {
value = massive;
}
}
if (value === massive) return find;
let localized = value[locale()] ?? value[defaultLocale] ?? find;
if (typeof localized === "function" && !fn) localized = localized();
return fn ? { make: localized } : localized;
}