From 8ed7e032052def3e4d5934f1146c5b8d53d0aba9 Mon Sep 17 00:00:00 2001 From: kmmiio99o Date: Wed, 1 Oct 2025 21:29:41 +0200 Subject: [PATCH] sure buddy :333 --- plugins/Commands/settings.tsx | 1297 ++++-------------- plugins/Commands/src/commands/lovefemboys.ts | 190 ++- 2 files changed, 451 insertions(+), 1036 deletions(-) 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;