// Reddit social sentiment intelligence. // Reddit API access requires OAuth. Runtime sweeps intentionally do not use // unauthenticated reddit.com .json scraping because it is unreliable and not // acceptable for production operation. import { safeFetch } from '../utils/fetch.mjs'; import '../utils/env.mjs'; function delay(ms) { return new Promise(r => setTimeout(r, ms)); } const USER_AGENT = 'Crucix/2.0 intelligence-engine'; const SUBREDDITS = [ 'worldnews', 'geopolitics', 'economics', 'wallstreetbets', 'commodities', ]; export function getRedditConfig(env = process.env) { const clientId = env.REDDIT_CLIENT_ID || ''; const clientSecret = env.REDDIT_CLIENT_SECRET || ''; const missing = []; if (!clientId) missing.push('REDDIT_CLIENT_ID'); if (!clientSecret) missing.push('REDDIT_CLIENT_SECRET'); return { clientId, clientSecret, configured: missing.length === 0, missing, }; } function credentialsMessage(missing) { return `Reddit requires OAuth. Register a script app at https://www.reddit.com/prefs/apps/ and set ${missing.join(' and ')} in .env`; } export async function getToken({ env = process.env, fetchImpl = globalThis.fetch } = {}) { const config = getRedditConfig(env); if (!config.configured) { return { ok: false, status: 'no_credentials', missing: config.missing, error: 'missing_reddit_oauth_credentials', message: credentialsMessage(config.missing), }; } try { const auth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64'); const res = await fetchImpl('https://www.reddit.com/api/v1/access_token', { method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': USER_AGENT, }, body: 'grant_type=client_credentials', }); if (!res.ok) { const body = await res.text().catch(() => ''); return { ok: false, status: 'auth_failed', error: `reddit_oauth_http_${res.status}`, message: `Reddit OAuth token request failed with HTTP ${res.status}`, detail: body.slice(0, 200), }; } const data = await res.json(); if (!data.access_token) { return { ok: false, status: 'auth_failed', error: 'reddit_oauth_missing_access_token', message: 'Reddit OAuth token response did not include an access token', }; } return { ok: true, status: 'ok', token: data.access_token }; } catch (e) { return { ok: false, status: 'auth_failed', error: 'reddit_oauth_request_failed', message: e.message, }; } } export async function getHot(subreddit, opts = {}) { const { limit = 10, token = null } = opts; if (!token) { return { status: 'no_credentials', error: 'reddit_oauth_required', message: 'Reddit source requires OAuth; unauthenticated reddit.com .json scraping is disabled', }; } return safeFetch(`https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1`, { source: 'Reddit', headers: { 'Authorization': `Bearer ${token}`, 'User-Agent': USER_AGENT, }, }); } function compactPost(child) { const d = child?.data; if (!d) return null; return { title: d.title, score: d.score ?? 0, comments: d.num_comments ?? 0, url: d.url, created: d.created_utc ? new Date(d.created_utc * 1000).toISOString() : null, }; } export async function briefing(opts = {}) { const { env = process.env, subreddits = SUBREDDITS, delayMs = 1000, fetchImpl = globalThis.fetch, } = opts; const tokenResult = await getToken({ env, fetchImpl }); if (!tokenResult.ok) { return { source: 'Reddit', timestamp: new Date().toISOString(), status: tokenResult.status, error: tokenResult.error, message: tokenResult.message, missing: tokenResult.missing || [], }; } const subredditResults = {}; const errors = []; for (const sub of subreddits) { const result = await getHot(sub, { limit: 10, token: tokenResult.token }); if (result?.error) { errors.push({ subreddit: sub, error: result.error }); subredditResults[sub] = []; if (delayMs > 0) await delay(delayMs); continue; } const children = result?.data?.children || []; subredditResults[sub] = children.map(compactPost).filter(Boolean); if (delayMs > 0) await delay(delayMs); } return { source: 'Reddit', timestamp: new Date().toISOString(), status: errors.length > 0 ? 'degraded' : 'ok', ...(errors.length > 0 ? { error: 'reddit_subreddit_fetch_failed', errors } : {}), subreddits: subredditResults, }; } if (process.argv[1]?.endsWith('reddit.mjs')) { const data = await briefing(); console.log(JSON.stringify(data, null, 2)); }