← back to Patty
app/api/orbit/post/route.ts
191 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { auditLog } from '@/lib/audit';
// Platform creation URLs and formatting templates
const PLATFORM_CONFIG: Record<string, { creation_url: string; name: string }> = {
moveon: {
creation_url: 'https://sign.moveon.org/petitions/new',
name: 'MoveOn',
},
change_org: {
creation_url: 'https://www.change.org/start-a-petition',
name: 'Change.org',
},
we_the_people: {
creation_url: 'https://petitions.whitehouse.gov/petition/create',
name: 'We The People',
},
custom: {
creation_url: '',
name: 'Custom Platform',
},
};
/**
* Format petition content for MoveOn
*/
function formatForMoveOn(petition: any): Record<string, unknown> {
return {
title: petition.title,
target: petition.target || 'U.S. Congress',
description: petition.body_text || petition.body_html?.replace(/<[^>]+>/g, '') || '',
category: petition.category || 'Other',
// MoveOn-specific fields
why_important: petition.summary || '',
suggested_tags: petition.tags || [],
signature_goal: petition.signature_goal || 1000,
};
}
/**
* Format petition content for Change.org
*/
function formatForChangeOrg(petition: any): Record<string, unknown> {
return {
title: petition.title,
decision_maker: petition.target || 'Congress',
petition_body: petition.body_text || petition.body_html?.replace(/<[^>]+>/g, '') || '',
// Change.org uses different field names
letter_body: petition.body_html || '',
tags: petition.tags || [],
category: petition.category || 'Government & Politics',
goal: petition.signature_goal || 1000,
image_url: petition.image_url || null,
};
}
/**
* Format petition content for We The People
*/
function formatForWeThePeople(petition: any): Record<string, unknown> {
// We The People has a 800-character limit for the body
const bodyText = petition.body_text || petition.body_html?.replace(/<[^>]+>/g, '') || '';
return {
title: petition.title.substring(0, 120),
body: bodyText.substring(0, 800),
// Must be addressed to the Administration
issues: petition.tags?.slice(0, 3) || [],
};
}
/**
* GET /api/orbit/post
* List cross-platform posts for a petition
*/
export async function GET(request: NextRequest) {
const user = verifyAuth(request);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const url = new URL(request.url);
const petitionId = url.searchParams.get('petition_id');
let sql = `SELECT op.*, p.title as petition_title
FROM orbit_posts op
JOIN petitions p ON op.petition_id = p.id`;
const params: unknown[] = [];
if (petitionId) {
params.push(petitionId);
sql += ` WHERE op.petition_id = $1`;
}
sql += ` ORDER BY op.created_at DESC`;
const result = await query(sql, params);
return NextResponse.json({ posts: result.rows });
} catch (err) {
console.error('[api/orbit/post] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });
}
}
/**
* POST /api/orbit/post
* Create a cross-platform petition post
* Since we cannot directly POST to MoveOn/Change.org APIs (requires OAuth/partner access),
* we generate formatted content and provide a creation URL + copy-ready content.
*/
export async function POST(request: NextRequest) {
const user = verifyAuth(request);
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
try {
const body = await request.json();
const { petition_id, platform, custom_url } = body;
if (!petition_id || !platform) {
return NextResponse.json({ error: 'petition_id and platform are required' }, { status: 400 });
}
const validPlatforms = Object.keys(PLATFORM_CONFIG);
if (!validPlatforms.includes(platform)) {
return NextResponse.json(
{ error: `Invalid platform. Must be one of: ${validPlatforms.join(', ')}` },
{ status: 400 },
);
}
// Fetch the petition
const petitionRes = await query(`SELECT * FROM petitions WHERE id = $1`, [petition_id]);
if (petitionRes.rows.length === 0) {
return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
}
const petition = petitionRes.rows[0];
// Generate platform-specific formatted content
let formattedContent: Record<string, unknown>;
switch (platform) {
case 'moveon':
formattedContent = formatForMoveOn(petition);
break;
case 'change_org':
formattedContent = formatForChangeOrg(petition);
break;
case 'we_the_people':
formattedContent = formatForWeThePeople(petition);
break;
case 'custom':
formattedContent = {
title: petition.title,
body: petition.body_text || petition.body_html?.replace(/<[^>]+>/g, '') || '',
target: petition.target,
url: custom_url || '',
};
break;
default:
formattedContent = { title: petition.title };
}
const platformCfg = PLATFORM_CONFIG[platform];
const externalUrl = custom_url || platformCfg.creation_url;
// Save the post record
const result = await query(
`INSERT INTO orbit_posts (petition_id, platform, external_url, status, post_data)
VALUES ($1, $2, $3, $4, $5)
RETURNING *`,
[petition_id, platform, externalUrl, 'draft', JSON.stringify(formattedContent)],
);
await auditLog('orbit.post_created', 'orbit_post', result.rows[0].id, {
petition_id,
platform,
petition_title: petition.title,
});
return NextResponse.json({
post: result.rows[0],
creation_url: externalUrl,
platform_name: platformCfg.name,
formatted_content: formattedContent,
instructions: `Copy the formatted content below and paste it into the ${platformCfg.name} petition creation form at: ${externalUrl}`,
});
} catch (err) {
console.error('[api/orbit/post] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 });
}
}