← back to Patty
app/api/petitions/[slug]/post/route.ts
252 lines
import { NextRequest, NextResponse } from 'next/server';
import { verifyAuth } from '@/lib/auth';
import { query } from '@/lib/db';
import { SISTER_AUTH, SISTER_OK, sisterUnconfigured } from '@/lib/sister-auth';
const PULSE_BASE = 'http://127.0.0.1:9845';
interface PostPlatform {
name: string;
enabled: boolean;
config?: Record<string, string>;
}
/**
* POST /api/petitions/[slug]/post
* Dispatches a petition to selected platforms via the Pulse agent.
* Logs each dispatch decision to the petition_posts table.
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> },
) {
const user = verifyAuth(request);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
if (!SISTER_OK) {
return sisterUnconfigured();
}
const { slug } = await params;
// Look up the petition
const petitionResult = await query(
`SELECT id, title, summary, body_text, slug, status FROM petitions WHERE slug = $1 LIMIT 1`,
[slug],
);
if (petitionResult.rows.length === 0) {
return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
}
const petition = petitionResult.rows[0];
let body: { platforms: PostPlatform[]; action?: string };
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { platforms, action = 'post' } = body;
if (!Array.isArray(platforms) || platforms.length === 0) {
return NextResponse.json({ error: 'platforms[] required' }, { status: 400 });
}
const enabledPlatforms = platforms.filter((p) => p.enabled);
if (enabledPlatforms.length === 0) {
return NextResponse.json({ error: 'No platforms enabled' }, { status: 400 });
}
const petitionUrl = `http://45.61.58.125:7460/petitions/${petition.slug}`;
const results: Array<{
platform: string;
status: string;
message: string;
post_id?: string;
}> = [];
// Build dispatch payload for the Pulse agent
const targetAgents: string[] = [];
const platformParams: Record<string, string> = {};
for (const p of enabledPlatforms) {
const name = p.name.toLowerCase();
// Patty "launch" is just marking the petition as featured/active -- handled locally
if (name === 'patty') {
try {
await query(
`UPDATE petitions SET status = 'active', distribution_platform = 'patty', updated_at = NOW() WHERE id = $1`,
[petition.id],
);
const insertResult = await query(
`INSERT INTO petition_posts (petition_id, platform, post_text, post_url, status, posted_at, platform_config, actor)
VALUES ($1, 'patty', $2, $3, 'posted', NOW(), $4, $5) RETURNING id`,
[
petition.id,
petition.summary || petition.title,
petitionUrl,
JSON.stringify(p.config || {}),
user,
],
);
results.push({
platform: 'patty',
status: 'posted',
message: 'Launched on Patty platform',
post_id: insertResult.rows[0]?.id,
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Unknown error';
results.push({ platform: 'patty', status: 'failed', message: msg });
}
continue;
}
// For social platforms, we dispatch through Pulse
const socialPlatforms = ['reddit', 'discord', 'twitter', 'bluesky', 'moveon', 'instagram', 'tiktok'];
if (!socialPlatforms.includes(name)) {
results.push({ platform: name, status: 'skipped', message: `Unknown platform: ${name}` });
continue;
}
// Collect platform-specific config
if (name === 'reddit' && p.config?.subreddit) {
platformParams.reddit_subreddit = p.config.subreddit;
}
if (name === 'discord' && p.config?.channel_id) {
platformParams.discord_channel_id = p.config.channel_id;
}
targetAgents.push(name);
// Log as pending -- we update after dispatch
try {
const insertResult = await query(
`INSERT INTO petition_posts (petition_id, platform, post_text, post_url, status, platform_config, actor)
VALUES ($1, $2, $3, $4, 'pending', $5, $6) RETURNING id`,
[
petition.id,
name,
petition.summary || petition.title,
petitionUrl,
JSON.stringify(p.config || {}),
user,
],
);
const postId = insertResult.rows[0]?.id;
results.push({
platform: name,
status: 'pending',
message: 'Queued for dispatch',
post_id: postId,
});
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Unknown error';
results.push({ platform: name, status: 'failed', message: msg });
}
}
// If we have social platform targets, dispatch through Pulse agent
if (targetAgents.length > 0) {
try {
const dispatchPayload = {
petition_id: petition.id,
title: petition.title,
body: petition.body_text || petition.summary || petition.title,
petition_url: petitionUrl,
target_agents: targetAgents,
platform_params: platformParams,
};
const pulseRes = await fetch(`${PULSE_BASE}/api/pulse/dispatch`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: SISTER_AUTH,
},
body: JSON.stringify(dispatchPayload),
signal: AbortSignal.timeout(15000),
});
const pulseData = await pulseRes.json();
// Update each post record with dispatch results
if (pulseData.results && Array.isArray(pulseData.results)) {
for (const pr of pulseData.results) {
const matchResult = results.find(
(r) => r.platform === pr.agent && r.post_id,
);
if (matchResult && matchResult.post_id) {
const newStatus = pr.status === 'dispatched' ? 'posted' : 'failed';
const errorMsg =
pr.status !== 'dispatched' ? pr.message || 'Dispatch failed' : null;
await query(
`UPDATE petition_posts SET status = $1, error_msg = $2, posted_at = CASE WHEN $1 = 'posted' THEN NOW() ELSE posted_at END WHERE id = $3`,
[newStatus, errorMsg, matchResult.post_id],
);
matchResult.status = newStatus;
matchResult.message =
pr.status === 'dispatched'
? 'Posted via Pulse'
: pr.message || 'Dispatch failed';
}
}
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Pulse unavailable';
// Mark all pending as failed
for (const r of results) {
if (r.status === 'pending' && r.post_id) {
await query(
`UPDATE petition_posts SET status = 'failed', error_msg = $1 WHERE id = $2`,
[msg, r.post_id],
);
r.status = 'failed';
r.message = msg;
}
}
}
}
// Log audit event
await query(
`INSERT INTO audit_events (event_type, entity_type, entity_id, actor, metadata)
VALUES ('petition_post', 'petition', $1, $2, $3)`,
[
petition.id,
user,
JSON.stringify({
platforms: enabledPlatforms.map((p) => p.name),
action,
results: results.map((r) => ({
platform: r.platform,
status: r.status,
})),
}),
],
);
return NextResponse.json({
petition_id: petition.id,
slug,
dispatched_at: new Date().toISOString(),
results,
summary: {
total: enabledPlatforms.length,
posted: results.filter((r) => r.status === 'posted').length,
failed: results.filter((r) => r.status === 'failed').length,
skipped: results.filter((r) => r.status === 'skipped').length,
},
});
}