Initial
This commit is contained in:
commit
c552956516
23 files changed
+5193
No files matched your search
@@ -0,0 +1,186 @@
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import type { BrowserContext } from 'playwright';
|
||||
import type { Config } from '../config/index.js';
|
||||
import type { Logger } from '../utils/logger.js';
|
||||
|
||||
export interface RedditPost {
|
||||
id: string;
|
||||
name: string; // fullname like "t3_xxxxx"
|
||||
subreddit: string;
|
||||
author: string;
|
||||
title: string;
|
||||
url: string;
|
||||
permalink: string;
|
||||
thumbnail: string;
|
||||
is_self: boolean;
|
||||
post_hint?: string;
|
||||
preview?: {
|
||||
images: Array<{
|
||||
source: { url: string; width: number; height: number };
|
||||
resolutions: Array<{ url: string; width: number; height: number }>;
|
||||
}>;
|
||||
};
|
||||
created_utc: number;
|
||||
over_18: boolean;
|
||||
spoiler: boolean;
|
||||
}
|
||||
|
||||
export class RedditApiClient {
|
||||
private client: AxiosInstance;
|
||||
private browserContext: BrowserContext | null = null;
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
private logger: Logger
|
||||
) {
|
||||
this.client = axios.create({
|
||||
baseURL: 'https://www.reddit.com',
|
||||
headers: {
|
||||
'User-Agent': config.reddit.userAgent,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
setBrowserContext(context: BrowserContext): void {
|
||||
this.browserContext = context;
|
||||
this.logger.debug('Browser context set for authenticated API requests');
|
||||
}
|
||||
|
||||
private async getCookieHeader(): Promise<string> {
|
||||
if (!this.browserContext) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const cookies = await this.browserContext.cookies('https://www.reddit.com');
|
||||
return cookies.map((c) => `${c.name}=${c.value}`).join('; ');
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to get cookies from browser context:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async getNewPosts(subreddit: string, limit = 25): Promise<RedditPost[]> {
|
||||
try {
|
||||
const cookieHeader = await this.getCookieHeader();
|
||||
const headers: Record<string, string> = {};
|
||||
if (cookieHeader) {
|
||||
headers['Cookie'] = cookieHeader;
|
||||
}
|
||||
|
||||
const response = await this.client.get(`/r/${subreddit}/new.json`, {
|
||||
params: { limit, raw_json: 1 },
|
||||
headers,
|
||||
});
|
||||
|
||||
const posts: RedditPost[] = response.data.data.children
|
||||
.map((child: { data: RedditPost }) => child.data)
|
||||
.filter((post: RedditPost) => this.isValidPost(post));
|
||||
|
||||
this.logger.debug(`Fetched ${posts.length} valid image posts from r/${subreddit}`);
|
||||
return posts;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
if (error.response?.status === 404) {
|
||||
this.logger.warn(`Subreddit r/${subreddit} not found`);
|
||||
return [];
|
||||
}
|
||||
if (error.response?.status === 403) {
|
||||
this.logger.warn(`Subreddit r/${subreddit} is private or quarantined`);
|
||||
return [];
|
||||
}
|
||||
if (error.response?.status === 429) {
|
||||
this.logger.warn(`Rate limited by Reddit, waiting...`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
this.logger.error(`Error fetching posts from r/${subreddit}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private isValidPost(post: RedditPost): boolean {
|
||||
// Skip self posts (text only)
|
||||
if (post.is_self) return false;
|
||||
|
||||
// Skip deleted/removed authors
|
||||
if (post.author === '[deleted]' || post.author === 'AutoModerator') return false;
|
||||
|
||||
// Skip NSFW if we want to be safe (configurable later)
|
||||
// if (post.over_18) return false;
|
||||
|
||||
// Check if it's an image post
|
||||
return this.hasImage(post);
|
||||
}
|
||||
|
||||
private hasImage(post: RedditPost): boolean {
|
||||
// Check post_hint
|
||||
if (post.post_hint === 'image') return true;
|
||||
|
||||
// Check for preview images
|
||||
if (post.preview?.images?.length) return true;
|
||||
|
||||
// Check URL patterns for direct image links
|
||||
const url = post.url.toLowerCase();
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
|
||||
if (imageExtensions.some((ext) => url.includes(ext))) return true;
|
||||
|
||||
// Check for known image hosts
|
||||
const imageHosts = ['i.redd.it', 'i.imgur.com', 'imgur.com'];
|
||||
if (imageHosts.some((host) => url.includes(host))) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getImageUrl(post: RedditPost): string | null {
|
||||
// Prefer high-quality preview images (already processed by Reddit)
|
||||
if (post.preview?.images?.[0]?.source?.url) {
|
||||
// Reddit encodes URLs in preview, need to decode HTML entities
|
||||
return this.decodeRedditUrl(post.preview.images[0].source.url);
|
||||
}
|
||||
|
||||
// Direct i.redd.it links
|
||||
if (post.url.includes('i.redd.it')) {
|
||||
return post.url;
|
||||
}
|
||||
|
||||
// Direct imgur links
|
||||
if (post.url.includes('i.imgur.com')) {
|
||||
return post.url;
|
||||
}
|
||||
|
||||
// Imgur album/page links - try to get direct image
|
||||
if (post.url.includes('imgur.com') && !post.url.includes('i.imgur.com')) {
|
||||
// Try adding .jpg extension
|
||||
const imgurId = post.url.split('/').pop()?.split('.')[0];
|
||||
if (imgurId) {
|
||||
return `https://i.imgur.com/${imgurId}.jpg`;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if URL has image extension
|
||||
const url = post.url.toLowerCase();
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
if (imageExtensions.some((ext) => url.includes(ext))) {
|
||||
return post.url;
|
||||
}
|
||||
|
||||
// Use thumbnail as last resort if it's a valid URL
|
||||
if (post.thumbnail && post.thumbnail.startsWith('http')) {
|
||||
return post.thumbnail;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private decodeRedditUrl(url: string): string {
|
||||
return url.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
getPostUrl(post: RedditPost): string {
|
||||
return `https://www.reddit.com${post.permalink}`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user