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