← back to Norma

agents/reddit-agent/skills/reply.js

150 lines

/**
 * Reddit Reply Skill
 *
 * Generates and posts contextual replies to Reddit posts and comments.
 * Uses AI to generate authentic, relevant responses.
 *
 * Rate limit: 1 per 5min
 */

const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generateReply } = require('../../shared/ai');
const { submitReply, getPostComments, hasCredentials } = require('../lib/reddit');

const AGENT = 'reddit-agent';
const PLATFORM = 'reddit';

const rateLimiter = new RateLimiter(AGENT, PLATFORM);

/**
 * @param {Object} body - Request body
 * @param {string} body.post_id - Reddit post/comment ID to reply to
 * @param {string} [body.context] - Context of the post being replied to
 * @param {string} [body.subreddit] - Subreddit name (for AI context)
 * @param {string} [body.petition_url] - Petition URL to mention if relevant
 * @param {string} [body.custom_reply] - Custom reply text (skips AI generation)
 * @returns {Promise<Object>}
 */
module.exports = async function reply(body) {
  if (!body.post_id) {
    throw new Error('post_id is required');
  }

  // Check rate limit
  const limit = await rateLimiter.checkLimit('reply');
  if (!limit.allowed) {
    await logAction({
      agent: AGENT,
      actionType: 'reply',
      platform: PLATFORM,
      targetId: body.post_id,
      channel: body.subreddit ? `r/${body.subreddit}` : null,
      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,
    };
  }

  // Get the post context if not provided
  let context = body.context || '';
  if (!context) {
    try {
      // Try to get the post text from its comments page
      const postIdClean = body.post_id.replace(/^t3_/, '').replace(/^t1_/, '');
      const comments = await getPostComments(postIdClean);
      // Use the first few comments as context
      context = comments
        .slice(0, 3)
        .map((c) => `u/${c.author}: ${c.body.substring(0, 200)}`)
        .join('\n\n');

      if (!context) {
        context = 'Advocacy related discussion on Reddit';
      }
    } catch (err) {
      console.warn(`[${AGENT}] Could not fetch context for ${body.post_id}:`, err.message);
      context = 'Advocacy related discussion on Reddit';
    }
  }

  // Add subreddit context if available
  if (body.subreddit) {
    context = `[r/${body.subreddit}]\n\n${context}`;
  }

  // Generate or use custom reply
  let replyText;
  let aiGenerated = false;

  if (body.custom_reply) {
    replyText = body.custom_reply;
  } else {
    aiGenerated = true;
    const generated = await generateReply({
      context,
      petitionUrl: body.petition_url || null,
      platform: 'reddit',
    });

    replyText = generated.reply;
  }

  // Ensure reply isn't too long (Reddit max is 10,000 chars, but keep it reasonable)
  if (replyText.length > 3000) {
    replyText = replyText.substring(0, 2997) + '...';
  }

  // Determine the full thing ID for Reddit API
  let thingId = body.post_id;
  if (!thingId.startsWith('t1_') && !thingId.startsWith('t3_')) {
    // Default to post (t3_) if no prefix
    thingId = `t3_${thingId}`;
  }

  // Submit the reply
  const result = await submitReply(thingId, replyText);

  // Record the rate limit
  await rateLimiter.recordAction('reply');

  // Log the action
  await logAction({
    agent: AGENT,
    actionType: 'reply',
    platform: PLATFORM,
    targetId: body.post_id,
    targetUrl: null,
    content: replyText,
    channel: body.subreddit ? `r/${body.subreddit}` : null,
    responseData: {
      reply_id: result.id,
      in_reply_to: body.post_id,
      ai_generated: aiGenerated,
      simulated: result.simulated || false,
      char_count: replyText.length,
    },
    status: result.id || result.simulated ? 'success' : 'error',
    errorMessage: result.error || null,
  });

  return {
    reply_id: result.id,
    in_reply_to: body.post_id,
    subreddit: body.subreddit || null,
    text: replyText,
    ai_generated: aiGenerated,
    simulated: result.simulated || false,
    posted: result.posted || false,
    credentials_configured: hasCredentials(),
    char_count: replyText.length,
    draft: result.draft || null,
  };
};