import type { TimePeriod } from "./discord/types.ts"; export interface RedditPost { id: string; title: string; author: string; score: number; num_comments: number; created_utc: number; permalink: string; url: string; selftext: string; is_gallery?: boolean; over_18: boolean; link_flair_text?: string; thumbnail?: string; preview?: { images: Array<{ source: { url: string; width: number; height: number; }; resolutions: Array<{ url: string; width: number; height: number; }>; }>; enabled: boolean; }; media?: { reddit_video?: { fallback_url: string; }; }; secure_media?: { reddit_video?: { fallback_url: string; }; }; } export interface RedditResponse { data: { children: Array<{ data: RedditPost; }>; }; } type SortType = "hot" | "top"; const fetchWithRetry = async ( url: string, maxRetries: number = 3, ): Promise => { for (let attempt = 0; attempt < maxRetries; attempt++) try { await new Promise((resolve) => setTimeout(resolve, Math.random() * 1000 + 500), ); const response = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", DNT: "1", Connection: "keep-alive", "Upgrade-Insecure-Requests": "1", }, }); return response; } catch (error) { if (attempt === maxRetries - 1) throw error; const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000; console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms ...`); await new Promise((resolve) => setTimeout(resolve, delay)); } throw new Error("Max retries exceeded"); }; export const fetchRedditPosts = async ( sort: SortType = "hot", time: TimePeriod = "day", ): Promise => { const url = `https://www.reddit.com/r/okbuddyumamusume/${sort}.json${sort === "top" ? `?t=${time}` : ""}`; const response = await fetchWithRetry(url); if (!response.ok) { let errorText = `Error fetching ${response.url}: ${response.status} ${response.statusText}`; try { const error = await response.text(); if ( error.includes("You've been blocked by network security") || error.includes("blocked by network security") ) throw new Error( "Reddit is blocking requests due to network security. This may be due to rate limiting or bot detection. Please try again later.", ); if (error) errorText = `${errorText} \n\n ${error}`; } catch (err) { if ( err instanceof Error && err.message.includes("blocked by network security") ) throw err; } throw new Error(errorText); } const data: RedditResponse = await response.json(); return data.data.children.map((post) => post.data); }; export const filterPostsByFlair = ( posts: RedditPost[], excludedFlairs: string[] = [], includedFlairs: string[] = [], ): RedditPost[] => { return posts.filter((post) => { if (post.is_gallery) return false; const hasMedia = post.media?.reddit_video?.fallback_url || post.secure_media?.reddit_video?.fallback_url || post.url; if (!hasMedia) return false; const postFlair = post.link_flair_text?.toLowerCase() || ""; const isNSFW = post.over_18 || postFlair.includes("nsfw"); if ( includedFlairs.length > 0 && includedFlairs.some((flair) => flair.toLowerCase() === "nsfw") ) if (includedFlairs.some((flair) => flair.toLowerCase() === "nsfw")) return isNSFW; if (isNSFW) return false; if (includedFlairs.length > 0) return includedFlairs.some((flair) => postFlair.includes(flair.toLowerCase()), ); if (excludedFlairs.length > 0) return !excludedFlairs.some((flair) => postFlair.includes(flair.toLowerCase()), ); return true; }); }; const getRandomPost = (posts: RedditPost[]): RedditPost => { if (posts.length === 0) throw new Error("No posts found matching the criteria"); const randomIndex = Math.floor(Math.random() * posts.length); return posts[randomIndex]; }; export const getCutePost = async (): Promise => { const posts = await fetchRedditPosts("hot"); const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]); return getRandomPost(filteredPosts); }; export const getRoleplayPost = async (): Promise => { const posts = await fetchRedditPosts("hot"); const filteredPosts = filterPostsByFlair(posts, [], ["roleplay"]); return getRandomPost(filteredPosts); }; export const getNSFWPost = async (): Promise => { const posts = await fetchRedditPosts("hot"); const filteredPosts = filterPostsByFlair(posts, [], ["nsfw"]); return getRandomPost(filteredPosts); }; export const getTopPost = async ( time: TimePeriod = "day", ): Promise => { const posts = await fetchRedditPosts("top", time); const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]); return getRandomPost(filteredPosts); };