← back to Norma
agents/reddit-agent/skills/discover.js
179 lines
/**
* Reddit Discover Skill
*
* Searches relevant subreddits for advocacy discussions.
* Uses Reddit's public JSON API (no auth required for reading).
*
* Rate limit: 60/min (Reddit API standard)
*/
const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { pushSocialFeed, pushTrending } = require('../../shared/pulse-reporter');
const { searchSubreddit, scoreRelevance, SUBREDDITS, KEYWORDS } = require('../lib/reddit');
const AGENT = 'reddit-agent';
const PLATFORM = 'reddit';
const rateLimiter = new RateLimiter(AGENT, PLATFORM);
/**
* @param {Object} body - Request body
* @param {string} [body.query='nonprofit advocacy'] - Search query
* @param {string[]} [body.subreddits] - Specific subreddits to search (defaults to all)
* @param {number} [body.max_results=25] - Max results per subreddit
* @param {string} [body.sort='new'] - Sort order: new, relevance, hot, top
* @param {boolean} [body.push_to_pulse=true] - Push results to Pulse
* @returns {Promise<Object>}
*/
module.exports = async function discover(body) {
const query = body.query || 'nonprofit advocacy';
const subreddits = body.subreddits || SUBREDDITS;
const maxResults = Math.min(body.max_results || 25, 100);
const sort = body.sort || 'new';
const pushToPulse = body.push_to_pulse !== false;
// Check rate limit
const limit = await rateLimiter.checkLimit('search');
if (!limit.allowed) {
await logAction({
agent: AGENT,
actionType: 'discover',
platform: PLATFORM,
content: query,
status: 'rate_limited',
errorMessage: `Rate limited. Retry after ${Math.ceil(limit.retryAfterMs / 1000)}s`,
});
return {
rate_limited: true,
retry_after_seconds: Math.ceil(limit.retryAfterMs / 1000),
current: limit.current,
max: limit.max,
};
}
const allResults = [];
for (const subreddit of subreddits) {
try {
const posts = await searchSubreddit(subreddit, query, sort, maxResults);
await rateLimiter.recordAction('search');
// Score each post for relevance
const scored = posts.map((post) => ({
...post,
relevance_score: scoreRelevance(post),
}));
// Filter to relevant posts only (score > 10)
const relevant = scored.filter((p) => p.relevance_score > 10);
// Sort by relevance
relevant.sort((a, b) => b.relevance_score - a.relevance_score);
allResults.push({
subreddit,
query,
total_found: posts.length,
relevant_count: relevant.length,
posts: relevant,
});
} catch (err) {
console.error(`[${AGENT}] Search failed for r/${subreddit}:`, err.message);
allResults.push({
subreddit,
query,
total_found: 0,
relevant_count: 0,
posts: [],
error: err.message,
});
}
}
// Aggregate stats
const totalFound = allResults.reduce((sum, r) => sum + r.total_found, 0);
const totalRelevant = allResults.reduce((sum, r) => sum + r.relevant_count, 0);
// Get top posts across all subreddits
const topPosts = allResults
.flatMap((r) => r.posts)
.sort((a, b) => b.relevance_score - a.relevance_score)
.slice(0, 10);
// Extract trending keywords from post titles
const keywordCounts = {};
allResults.flatMap((r) => r.posts).forEach((post) => {
const text = `${post.title} ${post.selftext}`.toLowerCase();
for (const kw of KEYWORDS) {
if (text.includes(kw.toLowerCase())) {
keywordCounts[kw] = (keywordCounts[kw] || 0) + 1;
}
}
});
const trending = Object.entries(keywordCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([topic, count]) => ({
topic,
platform: PLATFORM,
volume: count,
sentiment: 'neutral',
}));
// Push to Pulse
if (pushToPulse) {
if (trending.length > 0) {
await pushTrending(trending);
}
// Push high-relevance posts as social feed items
for (const post of topPosts.slice(0, 5)) {
await pushSocialFeed({
agent: AGENT,
platform: PLATFORM,
type: 'discussion',
title: `r/${post.subreddit}: ${post.title.substring(0, 100)}`,
url: post.permalink,
content: post.selftext.substring(0, 300),
metadata: {
post_id: post.id,
score: post.score,
num_comments: post.num_comments,
relevance_score: post.relevance_score,
subreddit: post.subreddit,
},
});
}
}
// Log the action
await logAction({
agent: AGENT,
actionType: 'discover',
platform: PLATFORM,
content: query,
responseData: {
query,
subreddits_searched: subreddits.length,
total_found: totalFound,
total_relevant: totalRelevant,
trending_keywords: trending.length,
},
status: 'success',
});
return {
query,
subreddits_searched: subreddits.length,
total_found: totalFound,
total_relevant: totalRelevant,
results: allResults,
top_posts: topPosts,
trending,
fetched_at: new Date().toISOString(),
};
};