View Source
The actual code and article source behind this site. No minification. No transpilation. What you see is what runs.
|
| /** |
| * Pure-PHP Reddit post fetcher for dispelled.ca. |
| * |
| * Uses Reddit's public multi-sub RSS (Atom) endpoint with a |
| * browser User-Agent. |
| * Reddit blocks generic PHP user-agents but permits Firefox UA requests. |
| * Results are cached for 30 minutes and kept as a stale fallback if Reddit |
| * temporarily rate-limits an anonymous RSS refresh. |
| */ |
| function publicRedditUrl(string $url): string |
| { |
| return preg_replace('#^https?://(?:old\.)?reddit\.com/#i', 'https://www.reddit.com/', $url) ?? $url; |
| } |
| function normalizeRedditItems(array $items): array |
| { |
| return array_map(function (array $item): array { |
| if (!empty($item['url'])) { |
| $item['url'] = publicRedditUrl($item['url']); |
| } |
| return $item; |
| }, $items); |
| } |
| function sortAndLimitRedditItems(array $items): array |
| { |
| usort($items, function (array $a, array $b): int { |
| $aTime = strtotime($a['publishedAt'] ?? '') ?: 0; |
| $bTime = strtotime($b['publishedAt'] ?? '') ?: 0; |
| return $bTime <=> $aTime; |
| }); |
| $limited = []; |
| $perSubreddit = []; |
| foreach ($items as $item) { |
| $subreddit = $item['subreddit'] ?? ''; |
| if (($perSubreddit[$subreddit] ?? 0) >= 20) continue; |
| $limited[] = $item; |
| $perSubreddit[$subreddit] = ($perSubreddit[$subreddit] ?? 0) + 1; |
| } |
| return $limited; |
| } |
| function mergeRedditItems(array $staleItems, array $freshItems): array |
| { |
| $refreshedSubreddits = array_unique(array_column($freshItems, 'subreddit')); |
| $retainedItems = array_filter( |
| $staleItems, |
| fn(array $item): bool => !in_array($item['subreddit'] ?? '', $refreshedSubreddits, true) |
| ); |
| $merged = []; |
| foreach (array_merge($retainedItems, $freshItems) as $item) { |
| $merged[$item['id']] = $item; |
| } |
| return sortAndLimitRedditItems(array_values($merged)); |
| } |
| function getRedditPosts(?string $requestedSub = null): array |
| { |
| $cacheFile = CACHE_DIR . '/reddit.json'; |
| $staleItems = []; |
| $allSubIds = [ |
| 'linux', 'programming', 'netsec', 'sysadmin', 'opensource', |
| 'raspberry_pi', 'embedded', 'meshtastic', 'python', 'openbsd', |
| 'pfsense', 'selfhosted', 'homelab', |
| ]; |
| $cacheIsFresh = false; |
| $missingSubIds = []; |
| if (file_exists($cacheFile)) { |
| $cached = json_decode(file_get_contents($cacheFile), true); |
| if (is_array($cached) && count($cached) > 0) { |
| $staleItems = sortAndLimitRedditItems(normalizeRedditItems($cached)); |
| if ($requestedSub !== null) { |
| $cachedForSub = array_values(array_filter( |
| $staleItems, |
| fn(array $item): bool => ($item['subreddit'] ?? '') === $requestedSub |
| )); |
| if ($cachedForSub !== []) return $cachedForSub; |
| } else { |
| $cacheIsFresh = (time() - filemtime($cacheFile)) < 1800; |
| $cachedSubIds = array_unique(array_column($staleItems, 'subreddit')); |
| $missingSubIds = array_values(array_diff($allSubIds, $cachedSubIds)); |
| if ($cacheIsFresh && $missingSubIds === []) { |
| return $staleItems; |
| } |
| } |
| } |
| } |
| $subs = $requestedSub |
| ?? ($cacheIsFresh && $missingSubIds !== [] ? $missingSubIds : $allSubIds); |
| if (is_array($subs)) $subs = implode('+', $subs); |
| $url = "https://reddit.com/r/{$subs}.rss?limit=100"; |
| $ctx = stream_context_create(['http' => [ |
| 'method' => 'GET', |
| 'header' => implode("\r\n", [ |
| 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0', |
| 'Accept: application/rss+xml, application/xml, text/xml;q=0.9, */*;q=0.8', |
| 'Accept-Language: en-US,en;q=0.5', |
| ]) . "\r\n", |
| 'timeout' => 12, |
| ]]); |
| $raw = @file_get_contents($url, false, $ctx); |
| if (!$raw) return $requestedSub !== null ? [] : $staleItems; |
| libxml_use_internal_errors(true); |
| $xml = simplexml_load_string($raw); |
| libxml_clear_errors(); |
| if (!$xml) return $requestedSub !== null ? [] : $staleItems; |
| $labelMap = [ |
| 'linux' => 'r/linux', |
| 'programming' => 'r/programming', |
| 'netsec' => 'r/netsec', |
| 'sysadmin' => 'r/sysadmin', |
| 'opensource' => 'r/opensource', |
| 'raspberry_pi'=> 'r/raspberry_pi', |
| 'embedded' => 'r/embedded', |
| 'meshtastic' => 'r/meshtastic', |
| 'python' => 'r/python', |
| 'openbsd' => 'r/openbsd', |
| 'pfsense' => 'r/pfSense', |
| 'selfhosted' => 'r/selfhosted', |
| 'homelab' => 'r/homelab', |
| ]; |
| $items = []; |
| foreach ($xml->entry as $entry) { |
| // Subreddit from <category term="linux"> |
| $sub = null; |
| foreach ($entry->category as $cat) { |
| $term = strtolower((string)$cat['term']); |
| if (isset($labelMap[$term])) { $sub = $term; break; } |
| } |
| if (!$sub) continue; |
| // URL from <link rel="alternate" href="..."> |
| $postUrl = ''; |
| foreach ($entry->link as $link) { |
| if ((string)$link['rel'] === 'alternate') { |
| $postUrl = (string)$link['href']; |
| break; |
| } |
| } |
| if (!$postUrl) $postUrl = (string)($entry->link['href'] ?? ''); |
| $postUrl = publicRedditUrl($postUrl); |
| // Description: strip HTML content, remove Reddit boilerplate |
| $desc = ''; |
| if (isset($entry->content)) { |
| $desc = strip_tags((string)$entry->content); |
| $desc = html_entity_decode($desc, ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
| $desc = preg_replace('/\s+/', ' ', trim($desc)); |
| $desc = preg_replace('/\s*submitted by\s+\/u\/\S+(\s+\[link\])?(\s+\[comments\])?/i', '', $desc); |
| $desc = preg_replace('/\s*\[link\]\s*(\[comments\])?/i', '', $desc); |
| $desc = trim($desc); |
| if (mb_strlen($desc) < 15) $desc = ''; |
| elseif (mb_strlen($desc) > 300) $desc = mb_substr($desc, 0, 300) . '…'; |
| } |
| $idRaw = (string)$entry->id; |
| $items[] = [ |
| 'id' => 'reddit-' . ($idRaw ?: md5($postUrl)), |
| 'title' => html_entity_decode((string)$entry->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'), |
| 'url' => $postUrl, |
| 'description' => $desc !== '' ? $desc : null, |
| 'subreddit' => $sub, |
| 'subredditLabel' => $labelMap[$sub], |
| 'publishedAt' => (string)$entry->updated, |
| ]; |
| } |
| if ($requestedSub === null && !$cacheIsFresh && $items !== []) { |
| $combinedBeforeHydration = array_merge($staleItems, $items); |
| $loadedSubIds = array_unique(array_column($combinedBeforeHydration, 'subreddit')); |
| $stillMissingSubIds = array_values(array_diff($allSubIds, $loadedSubIds)); |
| if ($stillMissingSubIds !== []) { |
| $hydratedItems = getRedditPosts(implode('+', $stillMissingSubIds)); |
| $items = array_merge($items, $hydratedItems); |
| } |
| } |
| if (count($items) > 0) { |
| if (!is_dir(CACHE_DIR)) mkdir(CACHE_DIR, 0755, true); |
| $mergedItems = mergeRedditItems($staleItems, $items); |
| file_put_contents($cacheFile, json_encode($mergedItems)); |
| return $requestedSub !== null ? sortAndLimitRedditItems($items) : $mergedItems; |
| } |
| return $items ?: ($requestedSub !== null ? [] : $staleItems); |
| } |
dispelled