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

117 lines
5.4 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, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
import { cli, Strategy } from '@jackwener/opencli/registry';
// Reddit subreddit names: 321 chars, letters/digits/underscore, must start
// with a letter. Accept an optional `r/` prefix and normalise it off.
const SUBREDDIT_NAME_RE = /^[A-Za-z][A-Za-z0-9_]{2,20}$/;
export function parseSubredditName(raw) {
let name = String(raw || '').trim();
if (!name) {
throw new ArgumentError(
'Subreddit name is required.',
'Pass a subreddit name like `python` (or `r/python`).',
);
}
if (name.startsWith('/r/')) name = name.slice(3);
else if (name.startsWith('r/')) name = name.slice(2);
if (!SUBREDDIT_NAME_RE.test(name)) {
throw new ArgumentError(
'Invalid subreddit name.',
'Subreddit names are 321 characters, start with a letter, and contain only letters, digits, and underscores.',
);
}
return name;
}
cli({
site: 'reddit',
name: 'subreddit-info',
access: 'read',
description: 'Show metadata for a Reddit subreddit (subscribers, description, created date, NSFW)',
domain: 'reddit.com',
strategy: Strategy.COOKIE,
browser: true,
args: [
{ name: 'name', type: 'string', required: true, positional: true, help: 'Subreddit name (no `r/` prefix needed)' },
],
columns: ['field', 'value'],
func: async (page, kwargs) => {
const sub = parseSubredditName(kwargs.name);
await page.goto('https://www.reddit.com');
// Banned / private / non-existent subreddits return a 404 envelope
// ({"error":404,"reason":"banned"|"private"|null,"message":"Not Found"}).
// We surface those as EmptyResultError so the table never contains a
// silent sentinel row. Intermediate keys avoid `field`/`value`.
const result = await page.evaluate(`(async () => {
try {
const sub = ${JSON.stringify(sub)};
const res = await fetch('/r/' + encodeURIComponent(sub) + '/about.json?raw_json=1', { credentials: 'include' });
if (res.status === 401 || res.status === 403 || res.status === 404) {
return { kind: 'missing', detail: 'Subreddit r/' + sub + ' was not found or is not accessible (HTTP ' + res.status + ').' };
}
if (!res.ok) {
return { kind: 'http', httpStatus: res.status, where: '/r/' + sub + '/about.json' };
}
const j = await res.json();
// Reddit may return an envelope-style 200 with {"kind":"Listing"} or
// an error body for quarantined / private subs. Identify "subreddit
// not found / not accessible" by the absence of data.display_name.
if (j?.error) {
if (j.error === 404 || j.reason === 'banned' || j.reason === 'private' || j.reason === 'quarantined') {
return { kind: 'missing', detail: 'Subreddit r/' + sub + ' is ' + (j.reason || 'unavailable') + '.' };
}
return { kind: 'http', httpStatus: j.error, where: '/r/' + sub + '/about.json (' + (j.reason || 'error') + ')' };
}
const info = j?.data;
if (!info || !info.display_name) {
return { kind: 'malformed', detail: 'Reddit returned malformed subreddit info for r/' + sub + ' (missing data.display_name).' };
}
return { kind: 'ok', info };
} catch (e) {
return { kind: 'exception', detail: String(e && e.message || e) };
}
})()`);
if (result?.kind === 'missing') {
throw new EmptyResultError(result.detail);
}
if (result?.kind === 'http') {
throw new CommandExecutionError(`HTTP ${result.httpStatus} from ${result.where}`);
}
if (result?.kind === 'malformed') {
throw new CommandExecutionError(result.detail);
}
if (result?.kind === 'exception') {
throw new CommandExecutionError(`subreddit-info failed: ${result.detail}`);
}
if (result?.kind !== 'ok') {
throw new CommandExecutionError(`Unexpected result from reddit subreddit-info: ${JSON.stringify(result)}`);
}
const s = result.info;
const created = s.created_utc
? new Date(s.created_utc * 1000).toISOString().split('T')[0]
: null;
const subscribers = typeof s.subscribers === 'number' ? s.subscribers : null;
const activeNow = typeof s.active_user_count === 'number'
? s.active_user_count
: (typeof s.accounts_active === 'number' ? s.accounts_active : null);
const description = typeof s.public_description === 'string'
? s.public_description.trim()
: '';
return [
{ field: 'Name', value: s.display_name_prefixed || ('r/' + s.display_name) },
{ field: 'Title', value: typeof s.title === 'string' ? s.title : null },
{ field: 'Subscribers', value: subscribers != null ? String(subscribers) : null },
{ field: 'Active Now', value: activeNow != null ? String(activeNow) : null },
{ field: 'NSFW', value: s.over18 ? 'Yes' : 'No' },
{ field: 'Type', value: typeof s.subreddit_type === 'string' ? s.subreddit_type : null },
{ field: 'Description', value: description || null },
{ field: 'Created', value: created },
{ field: 'URL', value: s.url ? 'https://www.reddit.com' + s.url : null },
];
},
});