View Source

The actual code and article source behind this site. No minification. No transpilation. What you see is what runs.

↓ Download PHP edition
reddit.php 201 lines
<?php
/**
* 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);
}