← back to Norma

agents/discord-agent/skills/discover.js

174 lines

/**
 * Discord Discover Skill
 *
 * Searches configured Discord channels for relevant advocacy discussions.
 * Queries channels from the sdcc_agent_monitors table.
 *
 * Rate limit: 5 per 5sec per channel
 */

const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { pushSocialFeed } = require('../../shared/pulse-reporter');
const { getRecentMessages, scoreRelevance, hasCredentials, KEYWORDS } = require('../lib/discord');
const { query: dbQuery } = require('../../shared/db');

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

const rateLimiter = new RateLimiter(AGENT, PLATFORM);

/**
 * @param {Object} body - Request body
 * @param {string[]} [body.channel_ids] - Specific channel IDs to search
 * @param {string[]} [body.keywords] - Keywords to filter by (default: advocacy keywords)
 * @param {number} [body.limit=50] - Max messages per channel
 * @param {boolean} [body.push_to_pulse=true] - Push results to Pulse
 * @returns {Promise<Object>}
 */
module.exports = async function discover(body) {
  const limit = Math.min(body.limit || 50, 100);
  const keywords = body.keywords || KEYWORDS;
  const pushToPulse = body.push_to_pulse !== false;

  // Check rate limit
  const rateCheck = await rateLimiter.checkLimit('post');
  if (!rateCheck.allowed) {
    await logAction({
      agent: AGENT,
      actionType: 'discover',
      platform: PLATFORM,
      status: 'rate_limited',
      errorMessage: `Rate limited. Retry after ${Math.ceil(rateCheck.retryAfterMs / 1000)}s`,
    });

    return {
      rate_limited: true,
      retry_after_seconds: Math.ceil(rateCheck.retryAfterMs / 1000),
      current: rateCheck.current,
      max: rateCheck.max,
    };
  }

  // Get channel IDs from body or database monitors
  const channelIds = new Set();

  if (body.channel_ids && body.channel_ids.length > 0) {
    body.channel_ids.forEach((id) => channelIds.add(id));
  }

  // Query database for configured monitors
  try {
    const { rows } = await dbQuery(
      `SELECT id, target, keywords
       FROM sdcc_agent_monitors
       WHERE agent = $1 AND platform = $2 AND is_active = true`,
      [AGENT, PLATFORM]
    );
    for (const row of rows) {
      if (row.target) {
        channelIds.add(row.target);
      }
    }
  } catch (err) {
    console.error(`[${AGENT}] Failed to fetch monitors:`, err.message);
  }

  if (channelIds.size === 0) {
    return {
      channels_searched: 0,
      total_messages: 0,
      relevant_count: 0,
      results: [],
      note: 'No channels configured. Add channel IDs via body.channel_ids or sdcc_agent_monitors table.',
    };
  }

  const allResults = [];

  for (const channelId of channelIds) {
    try {
      const messages = await getRecentMessages(channelId, limit);
      await rateLimiter.recordAction('post');

      // Score each message for relevance
      const scored = messages
        .filter((msg) => !msg.isBot) // Skip bot messages
        .map((msg) => ({
          ...msg,
          relevance_score: scoreRelevance(msg.content),
        }));

      // Filter to relevant messages
      const relevant = scored.filter((m) => m.relevance_score > 10);
      relevant.sort((a, b) => b.relevance_score - a.relevance_score);

      allResults.push({
        channelId,
        total_messages: messages.length,
        relevant_count: relevant.length,
        messages: relevant.slice(0, 20), // Top 20 relevant messages
      });
    } catch (err) {
      console.error(`[${AGENT}] Discover failed for channel ${channelId}:`, err.message);
      allResults.push({
        channelId,
        total_messages: 0,
        relevant_count: 0,
        messages: [],
        error: err.message,
      });
    }
  }

  const totalMessages = allResults.reduce((sum, r) => sum + r.total_messages, 0);
  const totalRelevant = allResults.reduce((sum, r) => sum + r.relevant_count, 0);

  // Push to Pulse if enabled
  if (pushToPulse) {
    const topMessages = allResults
      .flatMap((r) => r.messages)
      .sort((a, b) => b.relevance_score - a.relevance_score)
      .slice(0, 5);

    for (const msg of topMessages) {
      await pushSocialFeed({
        agent: AGENT,
        platform: PLATFORM,
        type: 'discussion',
        title: `${msg.author}: ${msg.content.substring(0, 80)}`,
        content: msg.content.substring(0, 300),
        metadata: {
          message_id: msg.id,
          author: msg.author,
          relevance_score: msg.relevance_score,
          created_at: msg.createdAt,
        },
      });
    }
  }

  // Log the action
  await logAction({
    agent: AGENT,
    actionType: 'discover',
    platform: PLATFORM,
    responseData: {
      channels_searched: channelIds.size,
      total_messages: totalMessages,
      total_relevant: totalRelevant,
      bot_connected: hasCredentials(),
    },
    status: 'success',
  });

  return {
    channels_searched: channelIds.size,
    total_messages: totalMessages,
    relevant_count: totalRelevant,
    results: allResults,
    bot_connected: hasCredentials(),
    fetched_at: new Date().toISOString(),
  };
};