Remove old plugins and add all-in-one commands plugin

This commit is contained in:
kmmiio99o 2025-09-27 19:17:06 +02:00
parent e213c562a5
commit dd931ab617
26 changed files with 299 additions and 791 deletions

1
.gitignore vendored
View file

@ -7,3 +7,4 @@ package-lock.json
Themer
RealThemer
ChatMesser
allcommands

View file

@ -1,6 +0,0 @@
{
"name": "BetterSpotifyIntegration",
"description": "Shrimply better integration with Spotify",
"vendetta": { },
"main": "src/index.tsx"
}

View file

@ -1,39 +0,0 @@
import { findByProps, findByStoreName } from "@vendetta/metro";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Image, View } from "react-native";
const FluxUtils = findByProps("useStateFromStores");
const SpotifyStore = findByStoreName("SpotifyStore");
const Button = findByProps("Sizes", "Looks") as any;
const SpotifySyncUtil = findByProps("play", "sync");
type ListenAlongButtonProps = Partial<{
activity: any;
user: any;
isCurrentUser: boolean;
activityButtonColor: string;
[key: string]: any;
}>
export default function ListenAlongButton(props: ListenAlongButtonProps) {
const currentPartyId = FluxUtils.useStateFromStores([SpotifyStore], () => SpotifyStore.getActivity()?.party?.id);
const alreadySync = props.isCurrentUser || props.activity?.party?.id === currentPartyId;
return <View pointerEvents={alreadySync ? "none" : "auto"}>
<Button
text={!alreadySync ? "Listen Along" : "You are already along for this ride"}
size={Button.Sizes.SMALL}
style={{
marginTop: 12,
backgroundColor: props.activityButtonColor,
opacity: alreadySync ? 0.5 : 1
}}
renderIcon={() => <Image
style={{ height: 16, width: 16, marginRight: 8, tintColor: "white" }}
source={getAssetIDByName("ic_headphone")}
/>}
onPress={() => void SpotifySyncUtil.sync(props.activity, props.user.id)}
/>
</View>;
}

View file

@ -1,33 +0,0 @@
import { findByName } from "@vendetta/metro";
import { i18n } from "@vendetta/metro/common";
import { before } from "@vendetta/patcher";
import { findInReactTree } from "@vendetta/utils";
import ListenAlongButton from "./components/ListenAlongButton";
let sectionPatch: () => void;
export default {
onLoad() {
const getListeningHeader = () => i18n.Messages.USER_ACTIVITY_HEADER_LISTENING?.intlMessage?.format({ name: "Spotify" });
sectionPatch = before("default", findByName("UserProfileSection", false), ([props]) => {
if (props.title !== getListeningHeader()) return;
const spotifyElement = findInReactTree(props.children, e => e?.style?.padding === 0);
const actionsIndex = spotifyElement?.children?.findIndex?.(e => e?.type?.name === "Actions");
if (actionsIndex && actionsIndex !== -1) {
const actions = spotifyElement.children[actionsIndex];
spotifyElement.children[actionsIndex] = <>
{actions}
{<ListenAlongButton {...actions.props} />}
</>;
}
});
},
onUnload() {
sectionPatch?.();
}
};

View file

@ -1,15 +0,0 @@
import { findByProps, findByStoreName } from "@vendetta/metro";
const FluxUtils = findByProps("useStateFromStores");
const SpotifyStore = findByStoreName("SpotifyStore");
const GamePartyStore = findByStoreName("GamePartyStore");
const UserStore = findByStoreName("UserStore");
export default function useListeningParty() {
const partySet = FluxUtils.useStateFromStores([GamePartyStore, SpotifyStore], () => {
const party = GamePartyStore.getParty(SpotifyStore.getActivity()?.party?.id);
return party?.size > 1 ? party : null;
});
return partySet ? [...partySet].map(id => UserStore.getUser(id)) : [];
}

View file

@ -1,7 +0,0 @@
{
"name": "HideGiftButton",
"description": "Hides the gift button in the chat input.",
"vendetta": {
"icon": "ic_gift_24px"
}
}

View file

@ -1,19 +0,0 @@
import { findByName } from "@vendetta/metro";
import { after } from "@vendetta/patcher";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { findInReactTree } from "@vendetta/utils";
const ChatInput = findByName("ChatInput");
let unpatch: () => boolean;
export default {
onLoad() {
const blockList = ["ic_thread_normal_24px", "ic_gift"].map(n => getAssetIDByName(n));
unpatch = after("render", ChatInput.prototype, (_, ret) => {
const input = findInReactTree(ret, t => "forceAnimateButtons" in t.props && t.props.actions);
input.props.actions = input.props.actions.filter(a => !blockList.includes(a.source));
});
},
onUnload: unpatch
};

View file

@ -1,7 +0,0 @@
{
"name": "HoldGIFSend",
"description": "Hold GIF links to chat input before sending them.",
"vendetta": {
"icon": "gif"
}
}

View file

@ -1,13 +0,0 @@
import { findByProps } from "@vendetta/metro";
import { instead } from "@vendetta/patcher";
const messageInputUtil = findByProps("selectGIF");
export const onUnload = instead("selectGIF", messageInputUtil, (args) => {
// args[1] never exists on older version, so we use it as an indicator
// prev: selectGIF([GIF]) now: selectGIF(currentChannelId, [GIF]);
if (args[1]) args[1] = args[1].url;
else args[0] = args[0].url;
messageInputUtil.insertText(...args);
});

View file

@ -1,7 +0,0 @@
{
"name": "MoreConfirm",
"description": "Prompts confirmations before making irreversible actions.",
"vendetta": {
"icon": "check"
}
}

View file

@ -1,88 +0,0 @@
/*!
* https://github.com/amsyarasyiq/aliucordrn-plugins/blob/6c0be39ca673c6ebcf48c0f397246b76d578aa26/MoarConfirm/index.tsx
*
* Copyright (c) 2022 Amsyar Rasyiq
*
* 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 <https://www.gnu.org/licenses/>.
*/
import { logger, patcher } from "@vendetta";
import { findByProps, findByStoreName } from "@vendetta/metro";
const dialog = findByProps("show", "confirm", "close");
const relationshipManager = findByProps("addRelationship");
const callManager = findByProps("handleStartCall");
const actionSheetManager = findByProps("hideActionSheet");
const UserStore = findByStoreName("UserStore");
export default new class MoreConfirm {
patches = [] as (() => void)[];
onLoad() {
this.patches.push(patcher.instead("handleStartCall", callManager, (args, orig) => {
const [{ rawRecipients: [{ username, discriminator }, multiple] }, isVideo] = args;
const action = isVideo ? "video call" : "call";
// if `multiple` is defined, it's probably a group call
dialog.show({
title: multiple ? `Start a group ${action}?` : `Start a ${action} with ${username}#${discriminator}?`,
body: multiple ? "Are you sure you want to start the group call?" : `Are you sure you want to ${action} ${username}#${discriminator}?`,
confirmText: "Yes",
cancelText: "Cancel",
confirmColor: "brand",
onConfirm: () => {
try {
orig(...args);
} catch (e) {
logger.error("Failed to start call", e);
}
},
});
}));
this.patches.push(patcher.instead("addRelationship", relationshipManager, (args, orig) => {
if (typeof args[0] !== "object" || !args[0].userId) return orig.apply(this, args);
const { username, discriminator } = UserStore.getUser(args[0].userId);
// This is hacky, but it *works*
const hideASInterval = setInterval(() => actionSheetManager.hideActionSheet(), 100);
setTimeout(() => clearInterval(hideASInterval), 3000);
const block = args[0].type === 2;
return new Promise(r => {
dialog.show({
title: `${block ? "Block" : "Friend"} ${username}#${discriminator}?`,
body: `Are you sure you want to ${block ? "block" : "friend"} ${username}#${discriminator}?`,
confirmText: "Yes",
cancelText: "Cancel",
confirmColor: "brand",
onConfirm: () => {
try {
r(orig.apply(this, args));
} catch (e) {
logger.error("Failed to add relationship", e);
} finally {
clearInterval(hideASInterval);
}
},
onCancel: () => void clearInterval(hideASInterval)
});
});
}));
}
onUnload() {
for (const unpatch of this.patches) unpatch();
}
};

View file

@ -1,7 +0,0 @@
{
"name": "NoAutoReplyMention",
"description": "Turns off auto mention when replying to someone.",
"vendetta": {
"icon": "ic_notif_off"
}
}

View file

@ -1,110 +0,0 @@
import { findByStoreName } from "@vendetta/metro";
import { NavigationNative, React, stylesheet } from "@vendetta/metro/common";
import { useProxy } from "@vendetta/storage";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { Forms } from "@vendetta/ui/components";
import { useEffect, useState } from "react";
import { FlatList, Image, ScrollView, TouchableOpacity } from "react-native";
import { SettingsSchema, settings } from ".";
const UserStore = findByStoreName("UserStore");
const { FormInput, FormDivider, FormText, FormIcon, FormRow, FormSection, FormSwitchRow } = Forms;
const styles = stylesheet.createThemedStyleSheet({
avatar: {
height: 36,
width: 36,
borderRadius: 18,
},
button: {
marginRight: 12,
}
});
function AddButton({ callback }: { callback: () => void }) {
return <TouchableOpacity onPress={callback}>
<FormText style={styles.button}>ADD</FormText>
</TouchableOpacity>;
}
function AddRow({ onFinish }: { onFinish: () => void }) {
const [value, setValue] = useState("");
const onPressCallback = () => {
if (!isNaN(parseInt(value)) && UserStore.getUser(value) && !settings.exempted.includes(value)) {
settings.exempted = [...settings.exempted, value];
setValue("");
}
onFinish();
};
return (
<FormRow
leading={<FormIcon source={getAssetIDByName("ic_add_friend")} />}
label={<FormInput autoFocus
title="User ID"
placeholder="123456789012345678"
value={value}
keyboardType="numeric"
onChangeText={(value: string) => setValue(value.replace(/[^0-9]/g, "").trim())}
returnKeyType="done"
/>}
trailing={(
<TouchableOpacity onPress={onPressCallback}>
<FormIcon source={getAssetIDByName("ic_add_24px")} />
</TouchableOpacity>
)}
/>
);
}
export default function Settings() {
useProxy(settings) as SettingsSchema;
const [shouldShowAdd, setShouldShowAdd] = useState(false);
const navigation = NavigationNative.useNavigation();
useEffect(() => {
navigation.setOptions({
headerRight: () => <AddButton callback={() => setShouldShowAdd(true)} />
});
}, []);
return (
<ScrollView>
<FormSwitchRow
label="Enable blacklist mode"
subLabel="If enabled, only users in the exempted list will be affected by the plugin."
leading={<FormIcon source={getAssetIDByName("ic_block")} />}
value={settings.isBlacklistMode}
onValueChange={(value: boolean) => settings.isBlacklistMode = value}
/>
<FormSection title="Exempted Users">
{settings.exempted?.length > 0 && <FlatList
data={settings.exempted}
keyExtractor={(item) => item}
renderItem={({ item }) => {
const user = UserStore.getUser(item);
if (!user) throw new Error("User not found/is not cached");
return (<FormRow
label={user.username}
leading={<Image style={styles.avatar} source={{ uri: user.getAvatarURL() }} />}
trailing={<TouchableOpacity onPress={() => {
settings.exempted = settings.exempted.filter((id) => id !== item);
}}>
<FormIcon source={getAssetIDByName("Small")} disableColor />
</TouchableOpacity>}
/>);
}}
ItemSeparatorComponent={FormDivider}
/>}
{shouldShowAdd && <>
<FormDivider />
<AddRow onFinish={() => setShouldShowAdd(false)} />
</>}
</FormSection>
</ScrollView>
);
}

View file

@ -1,26 +0,0 @@
// export const onUnload = globalThis.vendetta.patcher.before("createPendingReply", globalThis.vendetta.metro.findByProps("createPendingReply"), ([arg]) => arg.shouldMention &&= false);
import { patcher } from "@vendetta";
import { findByProps } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import Settings from "./Settings";
export interface SettingsSchema {
isBlacklistMode: boolean;
exempted: string[];
}
storage.isBlacklistMode ??= false;
storage.exempted ??= [];
export const settings = storage as SettingsSchema;
const ReplyManager = findByProps("createPendingReply");
export default {
settings: Settings,
onUnload: patcher.before("createPendingReply", ReplyManager, ([arg]) => {
const isListed = storage.exempted.includes(arg.message?.author?.id);
arg.shouldMention &&= storage.isBlacklistMode ? !isListed : isListed;
})
};

View file

@ -1,7 +0,0 @@
{
"name": "NoIdle",
"description": "Prevent status from idling when Discord is in the background.",
"vendetta": {
"icon": "StatusIdle"
}
}

View file

@ -1,8 +0,0 @@
import { patcher } from "@vendetta";
import { FluxDispatcher } from "@vendetta/metro/common";
export const onUnload = patcher.before("dispatch", FluxDispatcher, ([{ type }]) => {
if (type !== "IDLE") return;
return [{ type: "IDLE", idle: false }];
});

View file

@ -1,4 +0,0 @@
{
"name": "PyoncordInstaller",
"description": "installs pyoncord.exe (virus)"
}

View file

@ -1,28 +0,0 @@
import { plugin, plugins } from "@vendetta";
import { config } from "@vendetta/loader";
import { showConfirmationAlert } from "@vendetta/ui/alerts";
export default {
onLoad() {
plugins.removePlugin(plugin.id);
setTimeout(() => showConfirmationAlert({
title: "Install Pyoncord?",
content: "Loader's settings will be overriden to load Pyoncord instead. You may not be able to revert to Vendetta whenever Pyoncord breaks!",
confirmText: "Continue and Restart",
cancelText: "Nevermind",
onConfirm: this.install
}), 300);
},
async install() {
if (config.customLoadUrl) {
config.customLoadUrl.enabled = true;
config.customLoadUrl.url = "https://raw.githubusercontent.com/pyoncord/pyoncord/builds/pyoncord.js";
setTimeout(globalThis.nativeModuleProxy.BundleUpdaterManager.reload, 100);
} else {
alert("not a proper vendetta loader!");
}
}
};

View file

@ -1,7 +0,0 @@
{
"name": "RichPresence",
"description": "Sets Discord Rich Presence for your profile.",
"vendetta": {
"icon": "games"
}
}

View file

@ -1,174 +0,0 @@
import { findByProps, findByStoreName } from "@vendetta/metro";
import { React, stylesheet } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage";
import { semanticColors } from "@vendetta/ui";
import { Button, Forms } from "@vendetta/ui/components";
import { ScrollView } from "react-native";
const { FormSection, FormInput, FormRow, FormSwitch, FormText } = Forms;
const UserStore = findByStoreName("UserStore");
const profiles = findByProps("showUserProfile");
const styles = stylesheet.createThemedStyleSheet({
subText: {
fontSize: 14,
marginLeft: 16,
marginRight: 16,
color: semanticColors.TEXT_MUTED
},
textLink: {
color: semanticColors.TEXT_LINK,
}
});
export default function ConfigEditor({ selection }: { selection: string }) {
const settings = useProxy(storage.selections[selection]) as Activity;
return (
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ paddingBottom: 38 }}>
<Button
style={{ margin: 16 }}
color={"brand"}
size={Button.Sizes.MEDIUM}
look={Button.Looks.FILLED}
onPress={async () => {
profiles.showUserProfile({ userId: UserStore.getCurrentUser().id });
}}
text="Preview your profile"
/>
<FormSection title="Basic" titleStyleType="no_border">
<FormInput required autoFocus
title="Application Name"
value={settings.name}
placeholder="Discord"
onChange={v => settings.name = v}
/>
<FormInput required
title="Application ID"
value={settings.application_id}
placeholder="1054951789318909972"
onChange={v => settings.application_id = v}
keyboardType="numeric"
/>
<FormInput
title="Type"
value={settings.type}
placeholder="0"
onChange={v => settings.type = v}
keyboardType="numeric"
/>
<FormInput
title="Details"
value={settings.details}
placeholder="Competitive"
onChange={v => settings.details = v}
/>
<FormInput
title="State"
value={settings.state}
placeholder="Playing Solo"
onChange={v => settings.state = v}
/>
</FormSection>
<FormSection title="Images">
<FormInput
title="Large Image Asset Key or URL"
value={settings.assets.large_image}
placeholder="large_image_here"
onChange={v => settings.assets.large_image = v}
/>
<FormInput
title="Large Image Text"
value={settings.assets.large_text}
placeholder="Playing on Joe's lobby"
disabled={!settings.assets.large_image}
onChange={v => settings.assets.large_text = v}
/>
<FormInput
title="Small Image Asset Key or URL"
value={settings.assets.small_image}
placeholder="small_image_here"
onChange={v => settings.assets.small_image = v}
/>
<FormInput
title="Small Image Text"
value={settings.assets.small_text}
placeholder="Solo"
disabled={!settings.assets.small_image}
onChange={v => settings.assets.small_text = v}
/>
</FormSection>
<FormText style={styles.subText}>
{"Image assets key can be either a Discord app asset's name or a URL to an image."}
</FormText>
<FormSection title="Timestamps">
<FormRow
label="Enable timestamps"
subLabel="Set whether to show timestamps or not"
trailing={<FormSwitch
value={settings.timestamps._enabled}
onValueChange={v => settings.timestamps._enabled = v}
/>}
/>
<FormInput
title="Start Timestamp (milliseconds)"
value={settings.timestamps.start}
placeholder="1234567890"
disabled={!settings.timestamps._enabled}
onChange={v => settings.timestamps.start = v}
keyboardType="numeric"
/>
<FormInput
title="End Timestamp (milliseconds)"
value={settings.timestamps.end}
placeholder="1234567890"
disabled={!settings.timestamps._enabled}
onChange={v => settings.timestamps.end = v}
keyboardType="numeric"
/>
<FormRow
label="Use current time as start timestamp"
subLabel="This will override the start timestamp you set above"
disabled={!settings.timestamps._enabled}
onPress={() => settings.timestamps.start = String(Date.now())}
trailing={FormRow.Arrow}
/>
</FormSection>
<FormText style={styles.subText}>
Leaving start timestamp blank will use the time the Discord started.
</FormText>
<FormSection title="Buttons">
<FormInput
title="First Button Text"
value={settings.buttons[0].label}
placeholder="random link #1"
onChange={v => settings.buttons[0].label = v}
/>
<FormInput
title="First Button URL"
value={settings.buttons[0].url}
placeholder="https://discord.com/vanityurl/dotcom/steakpants/flour/flower/index11.html"
disabled={!settings.buttons[0].label}
onChange={v => settings.buttons[0].url = v}
/>
<FormInput
title="Second Button Text"
value={settings.buttons[1].label}
placeholder="random link #2"
onChange={v => settings.buttons[1].label = v}
/>
<FormInput
title="Second Button URL"
value={settings.buttons[1].url}
placeholder="https://youtu.be/w0AOGeqOnFY"
disabled={!settings.buttons[1].label}
onChange={v => settings.buttons[1].url = v}
/>
</FormSection>
</ScrollView>
);
}

View file

@ -1,40 +0,0 @@
import RPInstance from ".";
import { NavigationNative } from "@vendetta/metro/common";
import plugin from "@vendetta/plugin";
import { Forms } from "@vendetta/ui/components";
import ConfigEditor from "./ConfigEditor";
import { useEffect } from "react";
import { TouchableOpacity } from "react-native";
const { FormText } = Forms;
const storage = plugin.storage as typeof plugin.storage & {
selected: string;
selections: Record<string, Activity>;
};
function UpdateButton() {
async function onPressCallback() {
RPInstance.onUnload();
RPInstance.onLoad();
}
return <TouchableOpacity onPress={onPressCallback}>
<FormText style={{ marginRight: 12 }}>UPDATE</FormText>
</TouchableOpacity>;
}
export default function Settings() {
const navigation = NavigationNative.useNavigation();
useEffect(() => {
navigation.setOptions({
headerRight: UpdateButton
});
}, []);
return (
<ConfigEditor selection={storage.selected} />
);
}

View file

@ -1,121 +0,0 @@
import { logger, plugin } from "@vendetta";
import { findByProps } from "@vendetta/metro";
import { FluxDispatcher } from "@vendetta/metro/common";
import Settings from "./Settings";
import { cloneAndFilter } from "./utils";
enum ActivityTypes {
PLAYING = 0,
STREAMING = 1,
LISTENING = 2,
WATCHING = 3,
// CUSTOM = 4,
COMPETING = 5
}
const assetManager = findByProps("getAssetIds");
const pluginStartSince = Date.now();
const storage = plugin.storage as typeof plugin.storage & {
selected: string;
selections: Record<string, Activity>;
};
if (typeof storage.selected?.length !== "number") {
Object.assign(storage, {
selected: "default",
selections: {
default: createDefaultSelection()
}
});
}
function createDefaultSelection(): Activity {
return {
name: "Discord",
application_id: "1054951789318909972",
flags: 0,
type: ActivityTypes.PLAYING,
timestamps: {
_enabled: false,
start: pluginStartSince
},
assets: {},
// @ts-ignore
buttons: [{}, {}]
};
}
async function sendRequest(activity: Activity | null): Promise<{ [K in keyof Activity]: any } & Record<string, any>> {
if (typeof activity !== "object") throw new Error("Invalid activity");
if (activity) {
const timestampEnabled = activity?.timestamps?._enabled;
activity = cloneAndFilter(activity);
if (timestampEnabled) {
activity.timestamps.start ||= pluginStartSince;
} else {
delete activity.timestamps;
}
if (activity.assets) {
const args = [activity.application_id, [activity.assets.large_image, activity.assets.small_image]];
let assetIds = assetManager.getAssetIds(...args);
if (!assetIds.length) assetIds = await assetManager.fetchAssetIds(...args);
activity.assets.large_image = assetIds[0];
activity.assets.small_image = assetIds[1];
}
if (activity.buttons?.length) {
activity.buttons = activity.buttons.filter(x => x.label && x.url);
activity.buttons.length
? Object.assign(activity, {
metadata: { button_urls: activity.buttons.map(x => x.url) },
buttons: activity.buttons.map(x => x.label)
})
: delete activity.buttons;
} else {
delete activity.buttons;
}
}
FluxDispatcher.dispatch({
type: "LOCAL_ACTIVITY_UPDATE",
activity: activity,
pid: 1608,
socketId: "RichPresence@Vendetta"
});
return activity;
}
export default new class RichPresence {
onLoad() {
logger.log("Sending RPC request");
const currentActivity = storage.selections[storage.selected];
if (!currentActivity) {
throw new Error("Selected activity does not exist");
}
sendRequest(currentActivity)
.then(x => {
logger.log("RPC request sent");
logger.log(x);
}).catch(e => {
logger.error("An error occured while sending RPC request :(");
logger.error(e?.stack ?? e);
});
}
onUnload() {
logger.log("Stopping RPC request");
sendRequest(null);
}
settings = Settings;
};

View file

@ -1,25 +0,0 @@
function isValid(val: any) {
if (val && typeof val.length === "number") return val.length > 0;
return val || val === 0 || val === false;
}
// Deep clone and filter empty objects/values or keys starting with "_"
export function cloneAndFilter<T extends object>(obj: T): T {
const filter = (k: PropertyKey, v: any) => {
if (v === obj) return v;
if (typeof k === "string" && k.startsWith("_")) return undefined;
switch (typeof v) {
case "object": {
if (Array.isArray(v) && v.length === 0) return undefined;
const filteredEntries = Object.entries(v).filter((e) => isValid(e[1]));
return filteredEntries.length > 0 ? Object.fromEntries(filteredEntries) : undefined;
}
default:
return isValid(v) ? v : undefined;
}
};
return JSON.parse(JSON.stringify(obj, filter));
}

198
plugins/all-in-one/index.ts Normal file
View file

@ -0,0 +1,198 @@
import { registerCommand } from "@vendetta/commands";
import { findByStoreName, findByProps } from "@vendetta/metro";
import { storage } from "@vendetta/plugin";
import settings from "./settings";
const UserStore = findByStoreName("UserStore");
const MessageActions = findByProps("sendMessage");
let commands: Array<() => void> = [];
// Initialize storage with default values if not set
if (!storage.factSettings) {
storage.factSettings = {
sendAsReply: true,
includeCitation: false,
};
}
if (!storage.listSettings) {
storage.listSettings = {
pluginListAlwaysDetailed: false,
themeListAlwaysDetailed: false,
};
}
// API Functions
const uselessFact = async () => {
const response = await fetch(
"https://uselessfacts.jsph.pl/api/v2/facts/random",
);
const resp = await response.json();
return {
text: resp["text"],
source: resp["source"],
language: resp["language"],
};
};
const dogFact = async () => {
const response = await fetch("https://dogapi.dog/api/v2/facts?limit=1");
const resp = await response.json();
return {
text: resp["data"]["0"]["attributes"]["body"],
};
};
const catFact = async () => {
const response = await fetch("https://catfact.ninja/fact");
const resp = await response.json();
return {
text: resp["fact"],
length: resp["length"],
};
};
async function getPetPetData(image: string) {
const data = await fetch(
`https://api.obamabot.me/v2/image/petpet?image=${image.replace("webp", "png")}`,
);
const body = await data.json();
return body;
}
// Helper function to format fact response
const formatFactResponse = (fact: { text: string; source?: string }) => {
let response = fact.text;
if (storage.factSettings.includeCitation && fact.source) {
response += `\n\nSource: ${fact.source}`;
}
return response;
};
// Helper function to send message
const sendMessage = (
channelId: string,
content: string,
replyToId?: string,
) => {
const fixNonce = Date.now().toString();
const message = {
content,
...(replyToId && storage.factSettings.sendAsReply
? { messageReference: { messageId: replyToId } }
: {}),
};
MessageActions.sendMessage(channelId, message, undefined, {
nonce: fixNonce,
});
return { type: 4 }; // Acknowledge the command
};
// Command Definitions
const registerCommands = () => {
// Cat Facts Command
commands.push(
registerCommand({
name: "catfact",
displayName: "catfact",
description: "Sends a random cat fact.",
displayDescription: "Sends a random cat fact.",
applicationId: "-1",
inputType: 1,
type: 1,
execute: async (args, ctx) => {
const fact = await catFact();
return sendMessage(
ctx.channel.id,
formatFactResponse(fact),
ctx.message?.id,
);
},
}),
);
// Dog Facts Command
commands.push(
registerCommand({
name: "dogfact",
displayName: "dogfact",
description: "Sends a dog fact.",
displayDescription: "Sends a dog fact.",
applicationId: "-1",
inputType: 1,
type: 1,
execute: async (args, ctx) => {
const fact = await dogFact();
return sendMessage(
ctx.channel.id,
formatFactResponse(fact),
ctx.message?.id,
);
},
}),
);
// Useless Facts Command
commands.push(
registerCommand({
name: "useless",
displayName: "useless",
description: "Sends a useless fact.",
displayDescription: "Sends a useless fact.",
applicationId: "-1",
inputType: 1,
type: 1,
execute: async (args, ctx) => {
const fact = await uselessFact();
return sendMessage(
ctx.channel.id,
formatFactResponse(fact),
ctx.message?.id,
);
},
}),
);
// PetPet Command
commands.push(
registerCommand({
name: "petpet",
displayName: "petpet",
displayDescription: "PetPet someone",
description: "PetPet someone",
options: [
{
name: "user",
description: "The user(or their id) to be patted",
type: 6,
required: true,
displayName: "user",
displayDescription: "The user(or their id) to be patted",
},
],
execute: async (args: any, ctx: any) => {
const user = await UserStore.getUser(args[0].value);
const image = user.getAvatarURL(512);
const data = await getPetPetData(image);
return sendMessage(ctx.channel.id, data.url, ctx.message?.id);
},
applicationId: "-1",
inputType: 1,
type: 1,
}),
);
};
export default {
onLoad: () => {
console.log("[All-In-One Commands] Plugin loaded!");
registerCommands();
},
onUnload: () => {
console.log("[All-In-One Commands] Plugin unloaded!");
commands.forEach((unregister) => unregister());
commands = [];
},
settings,
};

View file

@ -0,0 +1,16 @@
{
"name": "All Commands",
"description": "A collection of useful commands including facts, petpet, and more.",
"authors": [
{
"name": "kmmiio99o",
"id": "879393496627306587"
}
],
"main": "index.ts",
"vendetta": {
"icon": "ic_badge_staff",
"original": "plugins/all-in-one"
},
"version": "1.0.0"
}

View file

@ -0,0 +1,84 @@
import { ReactNative } from "@vendetta/metro/common";
import { storage } from "@vendetta/plugin";
import { useProxy } from "@vendetta/storage";
import { getAssetIDByName } from "@vendetta/ui/assets";
import { ErrorBoundary, Forms } from "@vendetta/ui/components";
const { FormSwitchRow, FormIcon, FormSection, FormDivider } = Forms;
const Icons = {
List: getAssetIDByName('ic_list'),
Copy: getAssetIDByName('ic_copy_message_link'),
Settings: getAssetIDByName('ic_settings'),
Fact: getAssetIDByName('ic_info')
};
// Initialize storage with default values if not set
if (!storage.factSettings) {
storage.factSettings = {
sendAsReply: true,
includeCitation: false
};
}
if (!storage.listSettings) {
storage.listSettings = {
pluginListAlwaysDetailed: false,
themeListAlwaysDetailed: false
};
}
export default () => {
useProxy(storage);
return (
<ErrorBoundary>
<ReactNative.ScrollView style={{ flex: 1 }}>
{/* Plugin List Settings */}
<FormSection title="Plugin List Settings" titleStyleType="no_border">
<FormSwitchRow
label="Always send detailed plugin list"
subLabel='Always use detailed mode when listing plugins'
leading={<FormIcon source={Icons.List} />}
onValueChange={(value: boolean) => {
storage.listSettings.pluginListAlwaysDetailed = value;
}}
value={storage.listSettings.pluginListAlwaysDetailed}
/>
<FormDivider />
<FormSwitchRow
label="Always send detailed theme list"
subLabel='Always use detailed mode when listing themes'
leading={<FormIcon source={Icons.List} />}
onValueChange={(value: boolean) => {
storage.listSettings.themeListAlwaysDetailed = value;
}}
value={storage.listSettings.themeListAlwaysDetailed}
/>
</FormSection>
{/* Fact Command Settings */}
<FormSection title="Fact Commands Settings" titleStyleType="no_border">
<FormSwitchRow
label="Send as Reply"
subLabel='Send facts as a reply to the command message'
leading={<FormIcon source={Icons.Copy} />}
onValueChange={(value: boolean) => {
storage.factSettings.sendAsReply = value;
}}
value={storage.factSettings.sendAsReply}
/>
<FormDivider />
<FormSwitchRow
label="Include Source Citation"
subLabel='Include the source of the fact when available'
leading={<FormIcon source={Icons.Fact} />}
onValueChange={(value: boolean) => {
storage.factSettings.includeCitation = value;
}}
value={storage.factSettings.includeCitation}
/>
</FormSection>
</ReactNative.ScrollView>
</ErrorBoundary>
);
};