diff --git a/plugins/Commands/settings.tsx b/plugins/Commands/settings.tsx
index 2d2a7b4..0b2a39e 100644
--- a/plugins/Commands/settings.tsx
+++ b/plugins/Commands/settings.tsx
@@ -1,1007 +1,298 @@
-import { storage } from "@vendetta/plugin";
-import { useProxy } from "@vendetta/storage";
-import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
-import { NavigationNative } from "@vendetta/metro/common";
-import { semanticColors } from "@vendetta/ui";
-import { getAssetIDByName } from "@vendetta/ui/assets";
-import { Forms } from "@vendetta/ui/components";
-import { alerts } from "@vendetta/ui";
-import { showToast } from "@vendetta/ui/toasts";
+import { findByProps } from "@vendetta/metro";
-const { ScrollView } = RN;
-const { FormRow, FormSwitchRow } = Forms;
+const MessageActions = findByProps("sendMessage");
-// Initialize storage with default values - MOVED TO TOP
-storage.factSettings ??= {
- sendAsReply: true,
- includeCitation: false,
-};
-
-storage.listSettings ??= {
- pluginListAlwaysDetailed: false,
- themeListAlwaysDetailed: false,
-};
-
-storage.garySettings ??= {
- imageSource: "gary",
-};
-
-storage.enabledCommands ??= {
- catfact: true,
- dogfact: true,
- useless: true,
- petpet: true,
- pluginList: true,
- themeList: true,
- konoself: true,
- konosend: true,
- firstmessage: true,
- sysinfo: true,
- spotifyTrack: true,
- spotifyAlbum: true,
- spotifyArtists: true,
- spotifyCover: true,
- gary: true,
- lovefemboys: false, // Hidden command - disabled by default
-};
-
-storage.pendingRestart ??= false;
-
-// Hidden settings storage
-storage.hiddenSettings ??= {
- enabled: false,
- visible: false,
-};
-
-// Better Table Row Group Component
-function BetterTableRowGroup({
- title,
- icon,
- children,
- padding = false,
- onTitlePress,
-}: React.PropsWithChildren<{
- title?: string;
- icon?: number;
- padding?: boolean;
- onTitlePress?: () => void;
-}>) {
- const styles = stylesheet.createThemedStyleSheet({
- main: {
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderColor: semanticColors.BORDER_FAINT,
- borderWidth: 1,
- borderRadius: 16,
- overflow: "hidden",
- flex: 1,
- },
- titleContainer: {
- marginBottom: 8,
- marginHorizontal: 0,
- marginTop: 8,
- gap: 4,
- flexDirection: "row",
- alignItems: "center",
- },
- titlePressable: {
- flexDirection: "row",
- alignItems: "center",
- gap: 4,
- },
- icon: {
- width: 16,
- height: 16,
- marginTop: 1.5,
- tintColor: semanticColors.TEXT_MUTED,
- },
- titleText: {
- fontSize: 14,
- fontWeight: "600",
- color: semanticColors.TEXT_MUTED,
- },
- });
-
- return (
-
- {title && (
-
- {onTitlePress ? (
-
- {icon && (
-
- )}
-
- {title.toUpperCase()}
-
-
- ) : (
- <>
- {icon && (
-
- )}
-
- {title.toUpperCase()}
-
- >
- )}
-
- )}
-
- {padding ? (
-
- {children}
-
- ) : children}
-
-
- );
-}
-
-// Header Component with SILENT secret unlock
-function Header() {
- const [clickCounter, setClickCounter] = React.useState(0);
- const [clickTimeout, setClickTimeout] = React.useState(null);
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flexDirection: "column",
- alignItems: "center",
- paddingVertical: 24,
- paddingHorizontal: 16,
- },
- title: {
- fontSize: 28,
- fontWeight: "700",
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 4,
- },
- subtitle: {
- fontSize: 16,
- fontWeight: "500",
- color: semanticColors.TEXT_MUTED,
- },
- iconContainer: {
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- borderRadius: 16,
- padding: 12,
- marginBottom: 16,
- },
- icon: {
- width: 32,
- height: 32,
- tintColor: semanticColors.TEXT_NORMAL,
- },
- });
-
- const handleIconPress = () => {
- if (storage.hiddenSettings.enabled) {
- storage.hiddenSettings.visible = !storage.hiddenSettings.visible;
- showToast(
- `Hidden settings ${storage.hiddenSettings.visible ? "visible" : "hidden"}`,
- getAssetIDByName("SettingsIcon")
- );
- return;
- }
-
- if (clickTimeout) {
- clearTimeout(clickTimeout);
- }
-
- const newTimeout = setTimeout(() => {
- setClickCounter(0);
- }, 1000);
- setClickTimeout(newTimeout);
-
- const newCounter = clickCounter + 1;
- setClickCounter(newCounter);
-
- // Silent counting - no progress notifications
- if (newCounter < 10) {
- return; // Just count silently
- }
-
- // Unlocked! Only show this notification
- showToast("🔓 Hidden settings unlocked!", getAssetIDByName("CheckmarkIcon"));
- storage.hiddenSettings.enabled = true;
- storage.hiddenSettings.visible = true;
- setClickCounter(0);
- };
-
- return (
-
-
-
-
- Commands
- A collection of useful commands
-
- );
-}
-
-// Hidden Commands Settings Page
-function HiddenSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- warningText: {
- fontSize: 14,
- color: semanticColors.TEXT_DANGER,
- textAlign: "center",
- lineHeight: 20,
- fontWeight: "600",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- textAlign: "center",
- lineHeight: 20,
- marginTop: 8,
- },
- });
-
- return (
-
-
-
-
- These are hidden commands that may contain mature content or experimental features.
-
-
- Use at your own discretion. Commands in this section are disabled by default.
-
-
-
-
- }
- value={storage.enabledCommands.lovefemboys}
- onValueChange={(v) => {
- storage.enabledCommands.lovefemboys = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.hiddenSettings.visible}
- onValueChange={(v) => {
- storage.hiddenSettings.visible = v;
- }}
- />
- }
- onPress={() => {
- alerts.showConfirmationAlert({
- title: "Reset Hidden Settings",
- content: "This will hide the hidden settings section and disable all hidden commands. Are you sure?",
- confirmText: "Reset",
- onConfirm: () => {
- storage.hiddenSettings.enabled = false;
- storage.hiddenSettings.visible = false;
- storage.enabledCommands.lovefemboys = false;
- storage.pendingRestart = true;
- showToast("Hidden settings reset", getAssetIDByName("CheckmarkIcon"));
- },
- cancelText: "Cancel",
- });
- }}
- />
-
-
-
- );
-}
-
-// Facts Commands Settings Page - FIXED with local force update
-function FactsSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.factSettings.sendAsReply}
- onValueChange={(v) => {
- storage.factSettings.sendAsReply = v;
- }}
- />
- }
- value={storage.factSettings.includeCitation}
- onValueChange={(v) => {
- storage.factSettings.includeCitation = v;
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.catfact}
- onValueChange={(v) => {
- storage.enabledCommands.catfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.dogfact}
- onValueChange={(v) => {
- storage.enabledCommands.dogfact = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.useless}
- onValueChange={(v) => {
- storage.enabledCommands.useless = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Gary API Settings Page - FIXED with local force update
-function GaryAPIPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 16,
- textAlign: "center",
- },
- currentText: {
- fontSize: 14,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- },
- });
-
- const currentSource = storage.garySettings.imageSource;
-
- return (
-
-
-
- }
- value={storage.enabledCommands.gary}
- onValueChange={(v) => {
- storage.enabledCommands.gary = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
-
- Choose which API the /gary command should use to fetch images.
-
-
-
-
- }
- value={currentSource === "gary"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "gary";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "catapi"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "catapi";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "minker"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "minker";
- forceUpdate();
- }
- }}
- />
- }
- value={currentSource === "goober"}
- onValueChange={(v) => {
- if (v) {
- storage.garySettings.imageSource = "goober";
- forceUpdate();
- }
- }}
- />
-
-
-
-
- Currently using: {
- currentSource === "gary" ? "Gary API" :
- currentSource === "catapi" ? "Cat API" :
- currentSource === "minker" ? "Minker API" :
- currentSource === "goober" ? "Goober API" : "Gary API"
- }
-
-
-
-
- );
-}
-
-// List Settings Page - FIXED with local force update
-function ListSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.listSettings.pluginListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.pluginListAlwaysDetailed = v;
- }}
- />
- }
- value={storage.listSettings.themeListAlwaysDetailed}
- onValueChange={(v) => {
- storage.listSettings.themeListAlwaysDetailed = v;
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.pluginList}
- onValueChange={(v) => {
- storage.enabledCommands.pluginList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.themeList}
- onValueChange={(v) => {
- storage.enabledCommands.themeList = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Image Settings Page - FIXED with local force update
-function ImageSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.enabledCommands.petpet}
- onValueChange={(v) => {
- storage.enabledCommands.petpet = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.konoself}
- onValueChange={(v) => {
- storage.enabledCommands.konoself = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.konosend}
- onValueChange={(v) => {
- storage.enabledCommands.konosend = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Spotify Settings Page - FIXED with local force update
-function SpotifySettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.enabledCommands.spotifyTrack}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyTrack = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.spotifyAlbum}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyAlbum = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.spotifyArtists}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyArtists = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
- }
- value={storage.enabledCommands.spotifyCover}
- onValueChange={(v) => {
- storage.enabledCommands.spotifyCover = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
-
- These commands allow you to share your current Spotify activity in Discord. Make sure you have Spotify connected to Discord for these commands to work properly.
-
-
-
-
- );
-}
-
-// Other Settings Page - FIXED with local force update
-function OtherSettingsPage() {
- useProxy(storage);
- const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
-
- return (
-
-
-
- }
- value={storage.enabledCommands.firstmessage}
- onValueChange={(v) => {
- storage.enabledCommands.firstmessage = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- }
- value={storage.enabledCommands.sysinfo}
- onValueChange={(v) => {
- storage.enabledCommands.sysinfo = v;
- storage.pendingRestart = true;
- forceUpdate();
- }}
- />
-
-
-
- );
-}
-
-// Credits Page
-function CreditsPage() {
- const styles = stylesheet.createThemedStyleSheet({
- container: {
- flex: 1,
- backgroundColor: semanticColors.BACKGROUND_PRIMARY,
- },
- creditItem: {
- flexDirection: "row",
- alignItems: "center",
- paddingVertical: 16,
- paddingHorizontal: 20,
- backgroundColor: semanticColors.CARD_PRIMARY_BG,
- borderRadius: 12,
- marginHorizontal: 16,
- marginVertical: 6,
- shadowColor: semanticColors.BLACK,
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.1,
- shadowRadius: 2,
- elevation: 2,
- },
- avatar: {
- width: 48,
- height: 48,
- borderRadius: 24,
- marginRight: 16,
- backgroundColor: semanticColors.BACKGROUND_SECONDARY,
- },
- textContainer: {
- flex: 1,
- },
- commandText: {
- fontSize: 17,
- fontWeight: "600",
- color: semanticColors.TEXT_NORMAL,
- marginBottom: 4,
- },
- authorText: {
- fontSize: 15,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 2,
- },
- linkText: {
- fontSize: 13,
- color: semanticColors.TEXT_BRAND,
- fontWeight: "500",
- },
- infoText: {
- fontSize: 14,
- color: semanticColors.TEXT_MUTED,
- marginBottom: 20,
- textAlign: "center",
- lineHeight: 20,
- },
- versionText: {
- fontSize: 15,
- color: semanticColors.TEXT_NORMAL,
- textAlign: "center",
- fontWeight: "600",
- lineHeight: 22,
- },
- });
-
- const credits = [
- {
- command: "Facts Commands",
- author: "jdev082",
- avatar: "https://github.com/jdev082.png",
- github: "https://github.com/jdev082"
- },
- {
- command: "List Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight"
- },
- {
- command: "PetPet",
- author: "wolfieeee",
- avatar: "https://github.com/WolfPlugs.png",
- github: "https://github.com/WolfPlugs"
- },
- {
- command: "KonoChan Commands",
- author: "btmc727 & Rico040",
- avatar: "https://github.com/OTKUSteyler.png",
- github: "https://github.com/OTKUSteyler"
- },
- {
- command: "FirstMessage Command",
- author: "sapphire",
- avatar: "https://github.com/aeongdesu.png",
- github: "https://github.com/aeongdesu"
- },
- {
- command: "Sysinfo Command",
- author: "mugman",
- avatar: "https://github.com/mugman174.png",
- github: "https://github.com/mugman174"
- },
- {
- command: "Spotify Commands",
- author: "Kitomanari",
- avatar: "https://github.com/Kitosight.png",
- github: "https://github.com/Kitosight"
- },
- {
- command: "Gary Command",
- author: "Zach Orange",
- avatar: "https://github.com/Zach11111.png",
- github: "https://github.com/Zach11111"
- },
- ];
-
- const handleProfilePress = (githubUrl: string) => {
- RN.Linking.openURL(githubUrl);
- };
-
- return (
-
-
-
-
- Thanks to this developers for creating such a nice plugins!{'\n'}
- Tap on any developer to visit their GitHub profile.
-
-
-
- {credits.map((credit, index) => (
- handleProfilePress(credit.github)}
- android_ripple={{ color: semanticColors.ANDROID_RIPPLE, radius: 200 }}
- >
-
-
- {credit.command}
- by {credit.author}
- {credit.github.replace('https://github.com/', '@')}
-
-
- ))}
-
-
-
- Commands Plugin Collection{'\n'}
- Version 1.0.0
-
-
-
-
- );
-}
-
-// MAIN SETTINGS COMPONENT with hidden settings
-export default function Settings() {
- useProxy(storage);
- const navigation = NavigationNative.useNavigation();
-
- // Check for pending restart when unmounting
- React.useEffect(() => {
- return () => {
- if (storage.pendingRestart) {
- storage.pendingRestart = false;
- alerts.showConfirmationAlert({
- title: "Restart Required",
- content:
- "You have made changes to commands. Please restart Discord to apply these changes.",
- confirmText: "Okay",
- cancelText: null,
- });
- }
+interface RedditPost {
+ kind: string;
+ data: {
+ title: string;
+ url: string;
+ is_video: boolean;
+ over_18: boolean;
+ thumbnail: string;
+ thumbnail_width?: number;
+ thumbnail_height?: number;
+ preview?: {
+ images: Array<{
+ source: {
+ url: string;
+ width: number;
+ height: number;
+ };
+ resolutions: Array<{
+ url: string;
+ width: number;
+ height: number;
+ }>;
+ }>;
};
- }, []);
-
- return (
-
-
-
- {/* Command Categories */}
-
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Facts Commands",
- render: FactsSettingsPage,
- })}
- />
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "List Commands",
- render: ListSettingsPage,
- })}
- />
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Image Commands",
- render: ImageSettingsPage,
- })}
- />
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Gary Commands",
- render: GaryAPIPage,
- })}
- />
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Spotify Commands",
- render: SpotifySettingsPage,
- })}
- />
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Other Commands",
- render: OtherSettingsPage,
- })}
- />
-
-
- {/* Hidden Settings Section - Only visible when unlocked */}
- {storage.hiddenSettings?.enabled && storage.hiddenSettings?.visible && (
-
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Hidden Settings",
- render: HiddenSettingsPage,
- })}
- />
-
- )}
-
- {/* More Options */}
-
- }
- trailing={}
- onPress={() => navigation.push("VendettaCustomPage", {
- title: "Credits",
- render: CreditsPage,
- })}
- />
-
-
-
-
- );
+ secure_media?: {
+ oembed?: {
+ thumbnail_url: string;
+ type: string;
+ };
+ };
+ media_embed?: {
+ content: string;
+ };
+ url_overridden_by_dest?: string;
+ domain: string;
+ post_hint?: string;
+ };
}
+
+interface RedditResponse {
+ kind: string;
+ data: {
+ children: RedditPost[];
+ after: string;
+ dist: number;
+ };
+}
+
+function isValidImageUrl(url: string): boolean {
+ if (!url) return false;
+
+ const lowerUrl = url.toLowerCase();
+
+ // Check for direct image URLs
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
+ if (imageExtensions.some(ext => lowerUrl.includes(ext))) {
+ return true;
+ }
+
+ // Known image hosting domains
+ const imageHosts = [
+ 'i.redd.it',
+ 'i.imgur.com',
+ 'preview.redd.it'
+ ];
+
+ return imageHosts.some(host => lowerUrl.includes(host));
+}
+
+function extractImageUrl(post: RedditPost): string | null {
+ const postData = post.data;
+
+ // Skip videos and GIFs from redgifs/etc
+ if (postData.is_video || postData.domain.includes('redgifs.com') || postData.domain.includes('gfycat.com')) {
+ return null;
+ }
+
+ // Check post hint for image types
+ if (postData.post_hint && !['image', 'hosted:video'].includes(postData.post_hint)) {
+ return null;
+ }
+
+ // Try the main URL first
+ const mainUrl = postData.url;
+ if (isValidImageUrl(mainUrl)) {
+ return mainUrl;
+ }
+
+ // Try preview images
+ if (postData.preview?.images?.[0]) {
+ const previewImage = postData.preview.images[0];
+
+ // Try source image first (highest quality)
+ if (previewImage.source?.url) {
+ const sourceUrl = previewImage.source.url.replace(/&/g, '&');
+ if (isValidImageUrl(sourceUrl)) {
+ return sourceUrl;
+ }
+ }
+
+ // Try highest resolution preview
+ if (previewImage.resolutions?.length > 0) {
+ const highestRes = previewImage.resolutions[previewImage.resolutions.length - 1];
+ if (highestRes?.url) {
+ const resUrl = highestRes.url.replace(/&/g, '&');
+ if (isValidImageUrl(resUrl)) {
+ return resUrl;
+ }
+ }
+ }
+ }
+
+ // Try secure_media oembed thumbnail for some cases
+ if (postData.secure_media?.oembed?.thumbnail_url && postData.secure_media.oembed.type === 'photo') {
+ const thumbUrl = postData.secure_media.oembed.thumbnail_url;
+ if (isValidImageUrl(thumbUrl)) {
+ return thumbUrl;
+ }
+ }
+
+ // Try regular thumbnail as last resort (but only if it's not a default thumbnail)
+ if (postData.thumbnail &&
+ postData.thumbnail !== 'self' &&
+ postData.thumbnail !== 'default' &&
+ postData.thumbnail !== 'nsfw' &&
+ postData.thumbnail.startsWith('http')) {
+ if (isValidImageUrl(postData.thumbnail)) {
+ return postData.thumbnail;
+ }
+ }
+
+ return null;
+}
+
+async function getFemboyImage(includeNsfw: boolean = false): Promise<{ url: string; isNsfw: boolean; title: string } | null> {
+ try {
+ console.log(`[LoveFemboys] Fetching images, includeNsfw: ${includeNsfw}`);
+
+ // Fetch more posts to increase chances of finding images
+ const response = await fetch('https://reddit.com/r/femboys.json?limit=100&raw_json=1');
+ if (!response.ok) {
+ console.log('[LoveFemboys] Failed to fetch from Reddit API:', response.status, response.statusText);
+ return null;
+ }
+
+ const data: RedditResponse = await response.json();
+ console.log(`[LoveFemboys] Fetched ${data.data.children.length} posts`);
+
+ // Filter posts that are images and match NSFW preference
+ const validPosts = [];
+
+ for (const post of data.data.children) {
+ const postData = post.data;
+
+ console.log(`[LoveFemboys] Checking post: "${postData.title}" (NSFW: ${postData.over_18}, Domain: ${postData.domain})`);
+
+ // Filter by NSFW preference first
+ if (!includeNsfw && postData.over_18) {
+ console.log(`[LoveFemboys] Skipping NSFW post: ${postData.title}`);
+ continue;
+ }
+
+ // Try to extract image URL
+ const imageUrl = extractImageUrl(post);
+ if (imageUrl) {
+ console.log(`[LoveFemboys] Found valid image: ${imageUrl}`);
+ validPosts.push({
+ url: imageUrl,
+ isNsfw: postData.over_18,
+ title: postData.title
+ });
+ } else {
+ console.log(`[LoveFemboys] No valid image URL found for: ${postData.title}`);
+ }
+ }
+
+ console.log(`[LoveFemboys] Found ${validPosts.length} valid image posts (includeNsfw: ${includeNsfw})`);
+
+ if (validPosts.length === 0) {
+ console.log('[LoveFemboys] No suitable images found');
+ return null;
+ }
+
+ // Get random image
+ const randomPost = validPosts[Math.floor(Math.random() * validPosts.length)];
+ console.log(`[LoveFemboys] Selected: ${randomPost.title} - ${randomPost.url}`);
+
+ return randomPost;
+ } catch (error) {
+ console.error('[LoveFemboys] Error fetching image:', error);
+ return null;
+ }
+}
+
+export const lovefemboysCommand = {
+ name: "lovefemboys",
+ displayName: "lovefemboys",
+ description: "Get a random femboy image from r/femboys",
+ displayDescription: "Get a random femboy image from r/femboys",
+ options: [
+ {
+ name: "nsfw",
+ displayName: "nsfw",
+ description: "Include NSFW images",
+ displayDescription: "Include NSFW images",
+ type: 5, // Boolean
+ required: false,
+ },
+ {
+ name: "ephemeral",
+ displayName: "ephemeral",
+ description: "Send as ephemeral message (only you can see)",
+ displayDescription: "Send as ephemeral message (only you can see)",
+ type: 5, // Boolean
+ required: false,
+ }
+ ],
+ execute: async (args: any, ctx: any) => {
+ try {
+ const includeNsfw = args.find((arg: any) => arg.name === "nsfw")?.value || false;
+ const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
+
+ console.log(`[LoveFemboys] Command executed with nsfw: ${includeNsfw}, ephemeral: ${isEphemeral}`);
+
+ const result = await getFemboyImage(includeNsfw);
+
+ if (!result) {
+ const errorMessage = includeNsfw
+ ? "❌ Failed to fetch femboy image. Reddit might be having issues or no images available."
+ : "❌ No SFW femboy images found. Try with `nsfw:true` or try again later.";
+
+ if (isEphemeral) {
+ return {
+ type: 4,
+ data: {
+ content: errorMessage,
+ flags: 64, // Ephemeral flag
+ },
+ };
+ } else {
+ const fixNonce = Date.now().toString();
+ MessageActions.sendMessage(ctx.channel.id, { content: errorMessage }, void 0, {
+ nonce: fixNonce,
+ });
+ return { type: 4 };
+ }
+ }
+
+ const nsfwTag = result.isNsfw ? " 🔞" : " ✅";
+ const modeTag = includeNsfw ? " (NSFW allowed)" : " (SFW only)";
+ const content = `💖 **Femboy Image**${nsfwTag}${modeTag}\n*${result.title}*\n${result.url}`;
+
+ if (isEphemeral) {
+ return {
+ type: 4,
+ data: {
+ content,
+ flags: 64, // Ephemeral flag
+ },
+ };
+ } else {
+ const fixNonce = Date.now().toString();
+ MessageActions.sendMessage(ctx.channel.id, { content }, void 0, {
+ nonce: fixNonce,
+ });
+ return { type: 4 };
+ }
+ } catch (error) {
+ console.error('[LoveFemboys] Command error:', error);
+ const errorMessage = "❌ An error occurred while fetching femboy image. Check console for details.";
+
+ const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
+
+ if (isEphemeral) {
+ return {
+ type: 4,
+ data: {
+ content: errorMessage,
+ flags: 64, // Ephemeral flag
+ },
+ };
+ } else {
+ const fixNonce = Date.now().toString();
+ MessageActions.sendMessage(ctx.channel.id, { content: errorMessage }, void 0, {
+ nonce: fixNonce,
+ });
+ return { type: 4 };
+ }
+ }
+ },
+ applicationId: "-1",
+ inputType: 1,
+ type: 1,
+};
diff --git a/plugins/Commands/src/commands/lovefemboys.ts b/plugins/Commands/src/commands/lovefemboys.ts
index 5cb0ecb..0b2a39e 100644
--- a/plugins/Commands/src/commands/lovefemboys.ts
+++ b/plugins/Commands/src/commands/lovefemboys.ts
@@ -3,75 +3,193 @@ import { findByProps } from "@vendetta/metro";
const MessageActions = findByProps("sendMessage");
interface RedditPost {
+ kind: string;
data: {
title: string;
url: string;
is_video: boolean;
over_18: boolean;
+ thumbnail: string;
+ thumbnail_width?: number;
+ thumbnail_height?: number;
preview?: {
images: Array<{
source: {
url: string;
+ width: number;
+ height: number;
};
+ resolutions: Array<{
+ url: string;
+ width: number;
+ height: number;
+ }>;
}>;
};
+ secure_media?: {
+ oembed?: {
+ thumbnail_url: string;
+ type: string;
+ };
+ };
+ media_embed?: {
+ content: string;
+ };
url_overridden_by_dest?: string;
+ domain: string;
+ post_hint?: string;
};
}
interface RedditResponse {
+ kind: string;
data: {
children: RedditPost[];
+ after: string;
+ dist: number;
};
}
-async function getFemboyImage(includeNsfw: boolean = false): Promise {
+function isValidImageUrl(url: string): boolean {
+ if (!url) return false;
+
+ const lowerUrl = url.toLowerCase();
+
+ // Check for direct image URLs
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
+ if (imageExtensions.some(ext => lowerUrl.includes(ext))) {
+ return true;
+ }
+
+ // Known image hosting domains
+ const imageHosts = [
+ 'i.redd.it',
+ 'i.imgur.com',
+ 'preview.redd.it'
+ ];
+
+ return imageHosts.some(host => lowerUrl.includes(host));
+}
+
+function extractImageUrl(post: RedditPost): string | null {
+ const postData = post.data;
+
+ // Skip videos and GIFs from redgifs/etc
+ if (postData.is_video || postData.domain.includes('redgifs.com') || postData.domain.includes('gfycat.com')) {
+ return null;
+ }
+
+ // Check post hint for image types
+ if (postData.post_hint && !['image', 'hosted:video'].includes(postData.post_hint)) {
+ return null;
+ }
+
+ // Try the main URL first
+ const mainUrl = postData.url;
+ if (isValidImageUrl(mainUrl)) {
+ return mainUrl;
+ }
+
+ // Try preview images
+ if (postData.preview?.images?.[0]) {
+ const previewImage = postData.preview.images[0];
+
+ // Try source image first (highest quality)
+ if (previewImage.source?.url) {
+ const sourceUrl = previewImage.source.url.replace(/&/g, '&');
+ if (isValidImageUrl(sourceUrl)) {
+ return sourceUrl;
+ }
+ }
+
+ // Try highest resolution preview
+ if (previewImage.resolutions?.length > 0) {
+ const highestRes = previewImage.resolutions[previewImage.resolutions.length - 1];
+ if (highestRes?.url) {
+ const resUrl = highestRes.url.replace(/&/g, '&');
+ if (isValidImageUrl(resUrl)) {
+ return resUrl;
+ }
+ }
+ }
+ }
+
+ // Try secure_media oembed thumbnail for some cases
+ if (postData.secure_media?.oembed?.thumbnail_url && postData.secure_media.oembed.type === 'photo') {
+ const thumbUrl = postData.secure_media.oembed.thumbnail_url;
+ if (isValidImageUrl(thumbUrl)) {
+ return thumbUrl;
+ }
+ }
+
+ // Try regular thumbnail as last resort (but only if it's not a default thumbnail)
+ if (postData.thumbnail &&
+ postData.thumbnail !== 'self' &&
+ postData.thumbnail !== 'default' &&
+ postData.thumbnail !== 'nsfw' &&
+ postData.thumbnail.startsWith('http')) {
+ if (isValidImageUrl(postData.thumbnail)) {
+ return postData.thumbnail;
+ }
+ }
+
+ return null;
+}
+
+async function getFemboyImage(includeNsfw: boolean = false): Promise<{ url: string; isNsfw: boolean; title: string } | null> {
try {
- const response = await fetch('https://reddit.com/r/femboys.json');
+ console.log(`[LoveFemboys] Fetching images, includeNsfw: ${includeNsfw}`);
+
+ // Fetch more posts to increase chances of finding images
+ const response = await fetch('https://reddit.com/r/femboys.json?limit=100&raw_json=1');
if (!response.ok) {
- console.log('[LoveFemboys] Failed to fetch from Reddit API');
+ console.log('[LoveFemboys] Failed to fetch from Reddit API:', response.status, response.statusText);
return null;
}
const data: RedditResponse = await response.json();
+ console.log(`[LoveFemboys] Fetched ${data.data.children.length} posts`);
// Filter posts that are images and match NSFW preference
- const imagePosts = data.data.children.filter(post => {
+ const validPosts = [];
+
+ for (const post of data.data.children) {
const postData = post.data;
- // Skip videos
- if (postData.is_video) return false;
+ console.log(`[LoveFemboys] Checking post: "${postData.title}" (NSFW: ${postData.over_18}, Domain: ${postData.domain})`);
- // Check NSFW filter
- if (!includeNsfw && postData.over_18) return false;
+ // Filter by NSFW preference first
+ if (!includeNsfw && postData.over_18) {
+ console.log(`[LoveFemboys] Skipping NSFW post: ${postData.title}`);
+ continue;
+ }
- // Check if it's an image URL
- const url = postData.url_overridden_by_dest || postData.url;
- return url && (
- url.includes('.jpg') ||
- url.includes('.jpeg') ||
- url.includes('.png') ||
- url.includes('.gif') ||
- url.includes('i.redd.it') ||
- url.includes('i.imgur.com')
- );
- });
+ // Try to extract image URL
+ const imageUrl = extractImageUrl(post);
+ if (imageUrl) {
+ console.log(`[LoveFemboys] Found valid image: ${imageUrl}`);
+ validPosts.push({
+ url: imageUrl,
+ isNsfw: postData.over_18,
+ title: postData.title
+ });
+ } else {
+ console.log(`[LoveFemboys] No valid image URL found for: ${postData.title}`);
+ }
+ }
- if (imagePosts.length === 0) {
+ console.log(`[LoveFemboys] Found ${validPosts.length} valid image posts (includeNsfw: ${includeNsfw})`);
+
+ if (validPosts.length === 0) {
console.log('[LoveFemboys] No suitable images found');
return null;
}
// Get random image
- const randomPost = imagePosts[Math.floor(Math.random() * imagePosts.length)];
- const imageUrl = randomPost.data.url_overridden_by_dest || randomPost.data.url;
+ const randomPost = validPosts[Math.floor(Math.random() * validPosts.length)];
+ console.log(`[LoveFemboys] Selected: ${randomPost.title} - ${randomPost.url}`);
- // Handle Reddit preview images
- if (imageUrl.includes('reddit.com') && randomPost.data.preview?.images?.[0]?.source?.url) {
- return randomPost.data.preview.images[0].source.url.replace(/&/g, '&');
- }
-
- return imageUrl;
+ return randomPost;
} catch (error) {
console.error('[LoveFemboys] Error fetching image:', error);
return null;
@@ -106,10 +224,14 @@ export const lovefemboysCommand = {
const includeNsfw = args.find((arg: any) => arg.name === "nsfw")?.value || false;
const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;
- const imageUrl = await getFemboyImage(includeNsfw);
+ console.log(`[LoveFemboys] Command executed with nsfw: ${includeNsfw}, ephemeral: ${isEphemeral}`);
+
+ const result = await getFemboyImage(includeNsfw);
- if (!imageUrl) {
- const errorMessage = "❌ Failed to fetch femboy image. Try again later!";
+ if (!result) {
+ const errorMessage = includeNsfw
+ ? "❌ Failed to fetch femboy image. Reddit might be having issues or no images available."
+ : "❌ No SFW femboy images found. Try with `nsfw:true` or try again later.";
if (isEphemeral) {
return {
@@ -128,7 +250,9 @@ export const lovefemboysCommand = {
}
}
- const content = `💖 **Femboy Image** ${includeNsfw ? '(NSFW included)' : '(SFW only)'}\n${imageUrl}`;
+ const nsfwTag = result.isNsfw ? " 🔞" : " ✅";
+ const modeTag = includeNsfw ? " (NSFW allowed)" : " (SFW only)";
+ const content = `💖 **Femboy Image**${nsfwTag}${modeTag}\n*${result.title}*\n${result.url}`;
if (isEphemeral) {
return {
@@ -147,7 +271,7 @@ export const lovefemboysCommand = {
}
} catch (error) {
console.error('[LoveFemboys] Command error:', error);
- const errorMessage = "❌ An error occurred while fetching femboy image.";
+ const errorMessage = "❌ An error occurred while fetching femboy image. Check console for details.";
const isEphemeral = args.find((arg: any) => arg.name === "ephemeral")?.value || false;