Files
wehub-resource-sync 9b395f5cc3
Build Chrome Extension / build (push) Waiting to run
Trigger Website Rebuild (Docs Updated) / dispatch (push) Waiting to run
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

147 lines
6.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { cli, Strategy } from '@jackwener/opencli/registry';
const REDDIT_HOME_MAX_LIMIT = 100;
export function parseRedditHomeLimit(raw) {
if (raw === undefined || raw === null || raw === '') return 25;
const n = Number(raw);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > REDDIT_HOME_MAX_LIMIT) {
throw new ArgumentError(
`limit must be an integer in [1, ${REDDIT_HOME_MAX_LIMIT}].`,
`Got: ${raw}`,
);
}
return n;
}
export function decodeRedditHtml(value) {
if (typeof value !== 'string' || !value) return '';
return value
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#x27;/gi, "'")
.replace(/&#39;/g, "'");
}
export function extractRedditMedia(d) {
const galleryUrls = [];
const items = d?.gallery_data?.items;
const meta = d?.media_metadata;
if (Array.isArray(items) && meta && typeof meta === 'object') {
for (const item of items) {
const url = meta[item?.media_id]?.s?.u;
if (typeof url === 'string' && url) galleryUrls.push(decodeRedditHtml(url));
}
}
return {
post_hint: typeof d?.post_hint === 'string' ? d.post_hint : '',
url_overridden_by_dest: decodeRedditHtml(d?.url_overridden_by_dest || ''),
preview_image_url: decodeRedditHtml(d?.preview?.images?.[0]?.source?.url || ''),
gallery_urls: galleryUrls,
};
}
cli({
site: 'reddit',
name: 'home',
access: 'read',
description: 'Reddit personalized home feed (Best, requires login)',
domain: 'reddit.com',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'limit', type: 'int', default: 25, help: `Number of posts (1${REDDIT_HOME_MAX_LIMIT})` },
],
columns: ['rank', 'title', 'subreddit', 'score', 'comments', 'postId', 'author', 'url', 'post_hint', 'url_overridden_by_dest', 'preview_image_url', 'gallery_urls'],
func: async (page, kwargs) => {
const limit = parseRedditHomeLimit(kwargs.limit);
await page.goto('https://www.reddit.com');
// The Best feed is personalized only when logged in — for anonymous
// sessions Reddit returns a generic listing that overlaps with /r/all.
// To make `home` semantically meaningful (vs the public `frontpage`
// command) we require auth and surface the logged-out case via
// AuthRequiredError instead of silently returning the public feed.
//
// Two-pronged auth detection: HTTP 401/403 OR `me.data.name` missing
// on 200 (stale anonymous cookie session). See PR #1428 sediment.
//
// Intermediate object keys avoid `rank`/`title`/`subreddit`/etc. to
// sidestep the silent-column-drop audit; we use `entries` for the
// raw payload. See PR #1329 sediment "中间解析对象 key 不能跟
// columns 任一项重叠".
const result = await page.evaluate(`(async () => {
try {
const meRes = await fetch('/api/me.json', { credentials: 'include' });
if (meRes.status === 401 || meRes.status === 403) {
return { kind: 'auth', detail: 'Reddit /api/me.json returned HTTP ' + meRes.status };
}
if (!meRes.ok) {
return { kind: 'http', httpStatus: meRes.status, where: '/api/me.json' };
}
const me = await meRes.json();
if (!me?.data?.name) {
return { kind: 'auth', detail: 'Not logged in to reddit.com (no identity in /api/me.json)' };
}
const limit = ${JSON.stringify(limit)};
const res = await fetch('/best.json?limit=' + limit + '&raw_json=1', { credentials: 'include' });
if (res.status === 401 || res.status === 403) {
return { kind: 'auth', detail: 'Reddit /best.json returned HTTP ' + res.status };
}
if (!res.ok) {
return { kind: 'http', httpStatus: res.status, where: '/best.json' };
}
const j = await res.json();
const entries = j?.data?.children;
if (!Array.isArray(entries)) {
return { kind: 'http', httpStatus: 200, where: '/best.json (no data.children array)' };
}
return { kind: 'ok', entries };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'auth') {
throw new AuthRequiredError('reddit.com', result.detail);
}
if (result?.kind === 'http') {
throw new CommandExecutionError(`HTTP ${result.httpStatus} from ${result.where}`);
}
if (result?.kind === 'exception') {
throw new CommandExecutionError(`home failed: ${result.detail}`);
}
if (result?.kind !== 'ok') {
throw new CommandExecutionError(`Unexpected result from reddit home: ${JSON.stringify(result)}`);
}
const rows = [];
const entries = result.entries.slice(0, limit);
for (let i = 0; i < entries.length; i++) {
const d = entries[i]?.data;
if (!d || !d.id) continue;
rows.push({
rank: i + 1,
title: typeof d.title === 'string' ? d.title : null,
subreddit: typeof d.subreddit_name_prefixed === 'string' ? d.subreddit_name_prefixed : null,
score: typeof d.score === 'number' ? d.score : null,
comments: typeof d.num_comments === 'number' ? d.num_comments : null,
postId: d.id,
author: typeof d.author === 'string' ? d.author : null,
url: d.permalink ? 'https://www.reddit.com' + d.permalink : null,
...extractRedditMedia(d),
});
}
if (rows.length === 0) {
if (entries.length > 0) {
throw new CommandExecutionError('Reddit home feed entries were missing required post id anchors');
}
throw new EmptyResultError('Reddit returned no posts in the personalized home feed.');
}
return rows;
},
});