diff options
| author | Fuwn <[email protected]> | 2025-09-24 18:14:30 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-09-24 18:14:30 -0700 |
| commit | 2d987046d094cf5eb784c8d79d678bd3efa5eaf9 (patch) | |
| tree | dd37d395961d9a68e3e1293a89fb46992aab88d1 /packages/interactions/reddit.ts | |
| parent | style: Lint (diff) | |
| download | umabotdiscord-2d987046d094cf5eb784c8d79d678bd3efa5eaf9.tar.xz umabotdiscord-2d987046d094cf5eb784c8d79d678bd3efa5eaf9.zip | |
refactor: Move interactions client to packages directory
Diffstat (limited to 'packages/interactions/reddit.ts')
| -rw-r--r-- | packages/interactions/reddit.ts | 208 |
1 files changed, 208 insertions, 0 deletions
diff --git a/packages/interactions/reddit.ts b/packages/interactions/reddit.ts new file mode 100644 index 0000000..5b4ded7 --- /dev/null +++ b/packages/interactions/reddit.ts @@ -0,0 +1,208 @@ +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<Response> => { + 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<RedditPost[]> => { + 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<RedditPost> => { + const posts = await fetchRedditPosts("hot"); + const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]); + + return getRandomPost(filteredPosts); +}; + +export const getRoleplayPost = async (): Promise<RedditPost> => { + const posts = await fetchRedditPosts("hot"); + const filteredPosts = filterPostsByFlair(posts, [], ["roleplay"]); + + return getRandomPost(filteredPosts); +}; + +export const getNSFWPost = async (): Promise<RedditPost> => { + const posts = await fetchRedditPosts("hot"); + const filteredPosts = filterPostsByFlair(posts, [], ["nsfw"]); + + return getRandomPost(filteredPosts); +}; + +export const getTopPost = async ( + time: TimePeriod = "day", +): Promise<RedditPost> => { + const posts = await fetchRedditPosts("top", time); + const filteredPosts = filterPostsByFlair(posts, ["roleplay", "announcement"]); + + return getRandomPost(filteredPosts); +}; |