← back to Norma
agents/moveon-agent/skills/post.js
157 lines
/**
* MoveOn Post Skill
*
* Prepares petition data for MoveOn.org submission.
* Full browser automation would require Playwright — this skill
* prepares the petition draft, stores it in the pipeline, and
* returns the MoveOn URL template for manual or automated posting.
*
* Rate limit: 1/day
*/
const { RateLimiter } = require('../../shared/rate-limiter');
const { logAction } = require('../../shared/audit-logger');
const { generatePetitionContent } = require('../../shared/ai');
const { preparePetitionDraft } = require('../lib/moveon');
const { query: dbQuery } = require('../../shared/db');
const AGENT = 'moveon-agent';
const PLATFORM = 'moveon';
const rateLimiter = new RateLimiter(AGENT, PLATFORM);
/**
* @param {Object} body - Request body
* @param {string} [body.title] - Petition title (auto-generated if not provided)
* @param {string} [body.body] - Petition body (auto-generated if not provided)
* @param {string} [body.target='U.S. Department of Education'] - Petition target
* @param {string} [body.category='Education'] - Petition category
* @param {string} [body.topic='civic advocacy'] - Topic for AI generation
* @param {string} [body.tone='urgent'] - Tone for AI generation
* @param {string} [body.pipeline_id] - Existing pipeline entry ID to use
* @returns {Promise<Object>}
*/
module.exports = async function post(body) {
// Check rate limit
const limit = await rateLimiter.checkLimit('post');
if (!limit.allowed) {
await logAction({
agent: AGENT,
actionType: 'post',
platform: PLATFORM,
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,
};
}
let title = body.title;
let petitionBody = body.body;
let target = body.target || 'U.S. Department of Education';
let category = body.category || 'Education';
let talkingPoints = [];
let aiConfidence = null;
// If no title/body provided, generate via AI
if (!title || !petitionBody) {
const topic = body.topic || 'civic advocacy';
const tone = body.tone || 'urgent';
console.log(`[${AGENT}] Generating petition content via AI for topic: "${topic}"`);
const generated = await generatePetitionContent({
topic,
target,
category,
tone,
});
title = title || generated.title;
petitionBody = petitionBody || generated.body;
talkingPoints = generated.talking_points || [];
aiConfidence = generated.confidence || null;
target = generated.target || target;
}
// Prepare the MoveOn draft
const draft = preparePetitionDraft({
title,
body: petitionBody,
target,
category,
});
// Store in petition_pipeline if not already linked
let pipelineId = body.pipeline_id || null;
if (!pipelineId) {
try {
const result = await dbQuery(
`INSERT INTO petition_pipeline
(title, body, target, category, platform, tone, talking_points, status, ai_confidence, source_type)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'draft', $8, 'agent')
RETURNING id`,
[
title,
petitionBody,
target,
category,
PLATFORM,
body.tone || 'urgent',
talkingPoints,
aiConfidence,
]
);
pipelineId = result.rows[0].id;
} catch (err) {
console.error(`[${AGENT}] Failed to insert pipeline entry:`, err.message);
}
}
// Record the action
await rateLimiter.recordAction('post');
await logAction({
agent: AGENT,
actionType: 'post',
platform: PLATFORM,
content: title,
petitionId: pipelineId,
targetUrl: draft.previewUrl,
responseData: {
title,
target,
category,
aiGenerated: !body.title || !body.body,
aiConfidence,
talkingPoints,
pipelineId,
},
status: 'success',
});
return {
petition: {
title,
body: petitionBody,
target,
category,
talking_points: talkingPoints,
ai_confidence: aiConfidence,
},
draft: draft.formFields,
moveon_url: draft.previewUrl,
instructions: draft.instructions,
pipeline_id: pipelineId,
status: 'draft_prepared',
simulated: true,
posted: false,
note: 'Petition draft prepared in simulation mode. Full browser automation requires Playwright. Use the moveon_url or instructions to submit manually.',
};
};