> Array of post data. */ public function fetch_posts( string $subreddit, int $limit, string $time_filter = 'day' ): array { // Try RSS first — most reliable from datacenter IPs. $posts = $this->fetch_posts_via_rss( $subreddit, $limit ); if ( ! empty( $posts ) ) { return $posts; } // RSS failed — fall back to .json endpoints. PRAutoBlogger_Logger::instance()->info( sprintf( 'Reddit RSS returned 0 posts for r/%s — trying .json fallback.', $subreddit ), 'reddit' ); return $this->fetch_posts_via_json( $subreddit, $limit, $time_filter ); } /** * Fetch posts from a subreddit via Reddit's RSS/Atom feed. * * RSS endpoints work reliably from datacenter IPs where .json gets blocked. * Returns data normalized to the same format as .json for pipeline compatibility. * Some fields (score, num_comments) are unavailable in RSS and set to defaults. * * Side effects: HTTP request to Reddit. * * @param string $subreddit Subreddit name (without r/ prefix). * @param int $limit Max posts to fetch. * * @return array> Array of post data. */ public function fetch_posts_via_rss( string $subreddit, int $limit ): array { $url = sprintf( '%s/r/%s/hot.rss?limit=%d', self::BASE_URL, rawurlencode( $subreddit ), min( $limit, 100 ) ); $response = wp_remote_get( $url, array( 'timeout' => self::TIMEOUT_SECONDS, 'headers' => array( 'User-Agent' => self::USER_AGENT, 'Accept' => 'application/atom+xml, application/rss+xml', ), ) ); if ( is_wp_error( $response ) ) { PRAutoBlogger_Logger::instance()->error( 'Reddit RSS GET failed: ' . $response->get_error_message(), 'reddit' ); return array(); } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $status_code ) { PRAutoBlogger_Logger::instance()->error( sprintf( 'Reddit RSS HTTP %d for r/%s', $status_code, $subreddit ), 'reddit' ); return array(); } $body = wp_remote_retrieve_body( $response ); $parser = new PRAutoBlogger_Reddit_RSS_Parser(); return $parser->parse( $body, $subreddit ); } /** * Fetch hot posts from a subreddit via Reddit's .json endpoint (fallback). * * Datacenter IPs are often blocked by Reddit for .json endpoints (HTTP 403). * This method is only used as a fallback when RSS fails. * * Side effects: HTTP request, updates rate limit transient. * * @param string $subreddit Subreddit name (without r/ prefix). * @param int $limit Max posts to fetch (Reddit caps at 100). * @param string $time_filter Time filter for /hot (hour, day, week, month, year, all). * * @return array> Array of post data. */ public function fetch_posts_via_json( string $subreddit, int $limit, string $time_filter = 'day' ): array { $url = sprintf( '%s/r/%s/hot.json?limit=%d&t=%s&raw_json=1', self::BASE_URL, rawurlencode( $subreddit ), min( $limit, 100 ), rawurlencode( $time_filter ) ); $response = $this->api_get( $url ); if ( null === $response || ! isset( $response['data']['children'] ) ) { return array(); } $posts = array(); foreach ( $response['data']['children'] as $child ) { if ( isset( $child['data'] ) ) { $post = $child['data']; $post['data_source'] = 'reddit_json'; $posts[] = $post; } } return $posts; } /** * Fetch top comments for a specific post via Reddit's .json endpoint. * * Comments are always fetched via .json because RSS feeds don't include them. * Datacenter IP blocks may prevent this from working, in which case we * return an empty array (articles can still be generated from post titles alone). * * Side effects: HTTP request, updates rate limit transient. * * @param string $subreddit Subreddit name (without r/ prefix). * @param string $post_id Reddit post ID (without t3_ prefix). * @param int $limit Max comments to fetch. * * @return array> Array of comment data. */ public function fetch_comments( string $subreddit, string $post_id, int $limit = 5 ): array { $url = sprintf( '%s/r/%s/comments/%s.json?limit=%d&sort=top&depth=1&raw_json=1', self::BASE_URL, rawurlencode( $subreddit ), rawurlencode( $post_id ), min( $limit, 100 ) ); $response = $this->api_get( $url ); // Comments endpoint returns [post_listing, comments_listing]. if ( ! is_array( $response ) || ! isset( $response[1]['data']['children'] ) ) { return array(); } $comments = array(); foreach ( $response[1]['data']['children'] as $child ) { // Filter out "more" type entries, keep only actual comments (kind=t1). if ( isset( $child['data'] ) && 't1' === ( $child['kind'] ?? '' ) ) { $comments[] = $child['data']; } } return $comments; } /** * Check if at least one Reddit endpoint is reachable from this server. * * Tries RSS first (most reliable), then .json as fallback. * * Side effects: HTTP request. * * @return bool True if we got a valid response from either endpoint. */ public function is_available(): bool { return ( new PRAutoBlogger_Reddit_Availability() )->is_available(); } /** * Check if Reddit RSS endpoints are reachable. * * @return bool True if RSS endpoint returns HTTP 200. */ public function is_rss_available(): bool { return ( new PRAutoBlogger_Reddit_Availability() )->is_rss_available(); } /** * Check if Reddit .json endpoints are reachable. * * @return bool True if .json endpoint returns HTTP 200. */ public function is_json_available(): bool { return ( new PRAutoBlogger_Reddit_Availability() )->is_json_available(); } /** * Get current rate limit status. * * @return array{remaining: int, limit: int, resets_at: string} */ public function get_rate_limit_status(): array { return ( new PRAutoBlogger_Reddit_Availability() )->get_rate_limit_status(); } /** * Make an HTTP GET request to a Reddit .json endpoint. * * Delegates to PRAutoBlogger_Reddit_Availability which owns the HTTP * transport layer and rate-limit tracking. * * @param string $url Full URL with .json suffix and query params. * * @return array|null Decoded JSON, or null on failure. */ private function api_get( string $url ): ?array { return ( new PRAutoBlogger_Reddit_Availability() )->api_get( $url ); } }