summaryrefslogtreecommitdiff
path: root/packages/interactions/reddit.ts
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-09-24 18:14:30 -0700
committerFuwn <[email protected]>2025-09-24 18:14:30 -0700
commit2d987046d094cf5eb784c8d79d678bd3efa5eaf9 (patch)
treedd37d395961d9a68e3e1293a89fb46992aab88d1 /packages/interactions/reddit.ts
parentstyle: Lint (diff)
downloadumabotdiscord-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.ts208
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);
+};