← 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 });
  }
}