← back to Norma

agents/twitch-agent/skills/post.js

123 lines

/**
 * Twitch Post Skill
 *
 * Sends a chat message to a Twitch channel.
 * Can accept a pre-written message or generate one via AI.
 * Messages are concise for Twitch chat format (max 500 chars).
 *
 * Rate limit: 20/30sec (shared with reply)
 */

const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generateContent } = require('../../shared/ai');
const { sendMessage, initChat } = require('../lib/twitch');

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

const rateLimiter = new RateLimiter(AGENT, PLATFORM);

/**
 * @param {Object} body - Request body
 * @param {string} body.channel - Twitch channel name (required)
 * @param {string} [body.message] - Message to send (auto-generated if not provided)
 * @param {string} [body.topic='civic advocacy'] - Topic for AI generation
 * @param {string} [body.tone='casual'] - Tone: 'casual', 'informative', 'empathetic'
 * @returns {Promise<Object>}
 */
module.exports = async function post(body) {
  const channel = body.channel;

  if (!channel) {
    throw new Error('channel is required');
  }

  // Check rate limit
  const limit = await rateLimiter.checkLimit('post');
  if (!limit.allowed) {
    await logAction({
      agent: AGENT,
      actionType: 'post',
      platform: PLATFORM,
      channel,
      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,
    };
  }

  // Initialize chat client
  await initChat();

  let message = body.message;
  let aiGenerated = false;

  // Generate message via AI if not provided
  if (!message) {
    const topic = body.topic || 'civic advocacy';
    const tone = body.tone || 'casual';

    const prompt = `You are a community member in a Twitch chat. Write a single chat message about ${topic}.

TONE: ${tone}
MAX LENGTH: 200 characters (Twitch chat is fast-paced, keep it short)
RULES:
- Sound like a real person chatting, not a bot
- No hashtags (this is Twitch, not Twitter)
- No links (Twitch chat filters them)
- Be engaging and invite discussion
- Can reference current events about advocacy or civic engagement
- One sentence or two short ones max

Respond with ONLY the chat message text, nothing else.`;

    try {
      message = await generateContent(prompt, { maxTokens: 100, temperature: 0.9 });
      message = message.trim().replace(/^["']|["']$/g, ''); // Strip quotes if AI wraps in them
      aiGenerated = true;
    } catch (err) {
      console.error(`[${AGENT}] AI generation failed:`, err.message);
      // Fallback message
      message = 'Anyone else passionate about civic advocacy? Would love to hear what causes matter to you.';
      aiGenerated = true;
    }
  }

  // Send the message
  const result = await sendMessage(channel, message);

  // Record the action
  await rateLimiter.recordAction('post');
  await logAction({
    agent: AGENT,
    actionType: 'post',
    platform: PLATFORM,
    channel,
    content: message,
    targetUrl: `https://twitch.tv/${channel.replace('#', '')}`,
    responseData: {
      sent: result.sent,
      simulated: result.simulated,
      aiGenerated,
      messageLength: message.length,
    },
    status: 'success',
  });

  return {
    channel: result.channel,
    message: result.message,
    sent: result.sent,
    simulated: result.simulated,
    ai_generated: aiGenerated,
    posted_at: new Date().toISOString(),
  };
};