← back to Norma
agents/discord-agent/skills/reply.js
121 lines
/**
* Discord Reply Skill
*
* Generates and posts contextual replies to Discord messages.
* Uses AI to generate authentic, relevant responses.
*
* Rate limit: 5 per 5sec (same as post)
*/
const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generateReply } = require('../../shared/ai');
const { replyToMessage, hasCredentials } = require('../lib/discord');
const AGENT = 'discord-agent';
const PLATFORM = 'discord';
const rateLimiter = new RateLimiter(AGENT, PLATFORM);
/**
* @param {Object} body - Request body
* @param {string} body.channel_id - Discord channel ID
* @param {string} body.message_id - Message ID to reply to
* @param {string} [body.content] - Custom reply text (skips AI generation)
* @param {string} [body.context] - Context of the message being replied to
* @param {string} [body.petition_url] - Petition URL to mention if relevant
* @returns {Promise<Object>}
*/
module.exports = async function reply(body) {
if (!body.channel_id) {
throw new Error('channel_id is required');
}
if (!body.message_id) {
throw new Error('message_id is required');
}
// Check rate limit
const limit = await rateLimiter.checkLimit('post');
if (!limit.allowed) {
await logAction({
agent: AGENT,
actionType: 'reply',
platform: PLATFORM,
targetId: body.message_id,
channel: body.channel_id,
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,
};
}
// Generate or use custom reply
let replyText;
let aiGenerated = false;
if (body.content) {
replyText = body.content;
} else {
aiGenerated = true;
const context = body.context || 'Advocacy discussion in a Discord server';
const generated = await generateReply({
context,
petitionUrl: body.petition_url || null,
platform: 'discord',
});
replyText = generated.reply;
}
// Enforce Discord message limit (2000 chars)
if (replyText.length > 2000) {
replyText = replyText.substring(0, 1997) + '...';
}
// Post the reply
const result = await replyToMessage(body.channel_id, body.message_id, replyText);
// Record the rate limit
await rateLimiter.recordAction('post');
// Log the action
await logAction({
agent: AGENT,
actionType: 'reply',
platform: PLATFORM,
targetId: body.message_id,
content: replyText,
channel: body.channel_id,
responseData: {
reply_id: result.id,
channel_id: body.channel_id,
in_reply_to: body.message_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,
channel_id: body.channel_id,
in_reply_to: body.message_id,
text: replyText,
ai_generated: aiGenerated,
simulated: result.simulated || false,
posted: result.posted || false,
credentials_configured: hasCredentials(),
char_count: replyText.length,
draft: result.draft || null,
};
};