diff --git a/src/app/api/posts/[id]/like/route.ts b/src/app/api/posts/[id]/like/route.ts index 0878d06..bd3b6c0 100644 --- a/src/app/api/posts/[id]/like/route.ts +++ b/src/app/api/posts/[id]/like/route.ts @@ -41,7 +41,40 @@ export async function POST(request: Request, context: RouteContext) { return NextResponse.json({ error: 'Account restricted' }, { status: 403 }); } - // Check if post exists + // Handle swarm posts (format: swarm:domain:uuid) + if (postId.startsWith('swarm:')) { + const targetDomain = extractSwarmDomain(postId); + const originalPostId = postId.split(':')[2]; + + if (!targetDomain || !originalPostId) { + return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 }); + } + + // Deliver like directly to the origin node + const { deliverSwarmLike } = await import('@/lib/swarm/interactions'); + + const result = await deliverSwarmLike(targetDomain, { + postId: originalPostId, + like: { + actorHandle: user.handle, + actorDisplayName: user.displayName || user.handle, + actorAvatarUrl: user.avatarUrl || undefined, + actorNodeDomain: nodeDomain, + interactionId: crypto.randomUUID(), + timestamp: new Date().toISOString(), + }, + }); + + if (!result.success) { + console.error(`[Swarm] Like delivery failed: ${result.error}`); + return NextResponse.json({ error: 'Failed to deliver like to remote node' }, { status: 502 }); + } + + console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`); + return NextResponse.json({ success: true, liked: true }); + } + + // Local post - check if it exists const post = await db.query.posts.findFirst({ where: eq(posts.id, postId), }); @@ -109,7 +142,7 @@ export async function POST(request: Request, context: RouteContext) { } } - // SWARM-FIRST: Check if this is a swarm post and deliver directly + // If this is a cached swarm post (has swarm: apId), also deliver to origin if (isSwarmPost(post.apId)) { const targetDomain = extractSwarmDomain(post.apId); const originalPostId = extractSwarmPostId(post.apId!); @@ -135,7 +168,6 @@ export async function POST(request: Request, context: RouteContext) { console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`); } else { console.warn(`[Swarm] Like delivery failed: ${result.error}`); - // Could fall back to ActivityPub here if needed } } catch (err) { console.error('[Swarm] Error delivering like:', err); @@ -185,7 +217,38 @@ export async function DELETE(request: Request, context: RouteContext) { return NextResponse.json({ error: 'Account restricted' }, { status: 403 }); } - // Check if post exists + // Handle swarm posts (format: swarm:domain:uuid) + if (postId.startsWith('swarm:')) { + const targetDomain = extractSwarmDomain(postId); + const originalPostId = postId.split(':')[2]; + + if (!targetDomain || !originalPostId) { + return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 }); + } + + // Deliver unlike directly to the origin node + const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions'); + + const result = await deliverSwarmUnlike(targetDomain, { + postId: originalPostId, + unlike: { + actorHandle: user.handle, + actorNodeDomain: nodeDomain, + interactionId: crypto.randomUUID(), + timestamp: new Date().toISOString(), + }, + }); + + if (!result.success) { + console.error(`[Swarm] Unlike delivery failed: ${result.error}`); + return NextResponse.json({ error: 'Failed to deliver unlike to remote node' }, { status: 502 }); + } + + console.log(`[Swarm] Unlike delivered to ${targetDomain} for post ${originalPostId}`); + return NextResponse.json({ success: true, liked: false }); + } + + // Local post - check if it exists const post = await db.query.posts.findFirst({ where: eq(posts.id, postId), }); diff --git a/src/app/api/posts/[id]/repost/route.ts b/src/app/api/posts/[id]/repost/route.ts index 9fe4aba..bec1ba9 100644 --- a/src/app/api/posts/[id]/repost/route.ts +++ b/src/app/api/posts/[id]/repost/route.ts @@ -41,7 +41,41 @@ export async function POST(request: Request, context: RouteContext) { return NextResponse.json({ error: 'Account restricted' }, { status: 403 }); } - // Check if post exists + // Handle swarm posts (format: swarm:domain:uuid) + if (postId.startsWith('swarm:')) { + const targetDomain = extractSwarmDomain(postId); + const originalPostId = postId.split(':')[2]; + + if (!targetDomain || !originalPostId) { + return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 }); + } + + // Deliver repost directly to the origin node + const { deliverSwarmRepost } = await import('@/lib/swarm/interactions'); + + const result = await deliverSwarmRepost(targetDomain, { + postId: originalPostId, + repost: { + actorHandle: user.handle, + actorDisplayName: user.displayName || user.handle, + actorAvatarUrl: user.avatarUrl || undefined, + actorNodeDomain: nodeDomain, + repostId: crypto.randomUUID(), + interactionId: crypto.randomUUID(), + timestamp: new Date().toISOString(), + }, + }); + + if (!result.success) { + console.error(`[Swarm] Repost delivery failed: ${result.error}`); + return NextResponse.json({ error: 'Failed to deliver repost to remote node' }, { status: 502 }); + } + + console.log(`[Swarm] Repost delivered to ${targetDomain} for post ${originalPostId}`); + return NextResponse.json({ success: true, reposted: true }); + } + + // Local post - check if it exists const originalPost = await db.query.posts.findFirst({ where: eq(posts.id, postId), }); @@ -196,12 +230,44 @@ export async function DELETE(request: Request, context: RouteContext) { try { const user = await requireAuth(); const { id: postId } = await context.params; + const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000'; if (user.isSuspended || user.isSilenced) { return NextResponse.json({ error: 'Account restricted' }, { status: 403 }); } - // Check if original post exists + // Handle swarm posts (format: swarm:domain:uuid) + if (postId.startsWith('swarm:')) { + const targetDomain = extractSwarmDomain(postId); + const originalPostId = postId.split(':')[2]; + + if (!targetDomain || !originalPostId) { + return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 }); + } + + // Deliver unrepost directly to the origin node + const { deliverSwarmUnrepost } = await import('@/lib/swarm/interactions'); + + const result = await deliverSwarmUnrepost(targetDomain, { + postId: originalPostId, + unrepost: { + actorHandle: user.handle, + actorNodeDomain: nodeDomain, + interactionId: crypto.randomUUID(), + timestamp: new Date().toISOString(), + }, + }); + + if (!result.success) { + console.error(`[Swarm] Unrepost delivery failed: ${result.error}`); + return NextResponse.json({ error: 'Failed to deliver unrepost to remote node' }, { status: 502 }); + } + + console.log(`[Swarm] Unrepost delivered to ${targetDomain} for post ${originalPostId}`); + return NextResponse.json({ success: true, reposted: false }); + } + + // Local post - check if original post exists const originalPost = await db.query.posts.findFirst({ where: eq(posts.id, postId), }); diff --git a/src/app/api/swarm/interactions/unrepost/route.ts b/src/app/api/swarm/interactions/unrepost/route.ts new file mode 100644 index 0000000..f338713 --- /dev/null +++ b/src/app/api/swarm/interactions/unrepost/route.ts @@ -0,0 +1,67 @@ +/** + * Swarm Unrepost Endpoint + * + * POST: Receive an unrepost from another swarm node + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { db, posts } from '@/db'; +import { eq } from 'drizzle-orm'; +import { z } from 'zod'; + +const swarmUnrepostSchema = z.object({ + postId: z.string().uuid(), + unrepost: z.object({ + actorHandle: z.string(), + actorNodeDomain: z.string(), + interactionId: z.string(), + timestamp: z.string(), + }), +}); + +/** + * POST /api/swarm/interactions/unrepost + * + * Receives an unrepost from another swarm node. + */ +export async function POST(request: NextRequest) { + try { + if (!db) { + return NextResponse.json({ error: 'Database not available' }, { status: 503 }); + } + + const body = await request.json(); + const data = swarmUnrepostSchema.parse(body); + + // Find the target post + const post = await db.query.posts.findFirst({ + where: eq(posts.id, data.postId), + }); + + if (!post) { + return NextResponse.json({ error: 'Post not found' }, { status: 404 }); + } + + if (post.isRemoved) { + return NextResponse.json({ error: 'Post not found' }, { status: 404 }); + } + + // Decrement repost count + await db.update(posts) + .set({ repostsCount: Math.max(0, post.repostsCount - 1) }) + .where(eq(posts.id, data.postId)); + + console.log(`[Swarm] Received unrepost from ${data.unrepost.actorHandle}@${data.unrepost.actorNodeDomain} on post ${data.postId}`); + + return NextResponse.json({ + success: true, + message: 'Unrepost received', + }); + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: 'Invalid request', details: error.issues }, { status: 400 }); + } + console.error('[Swarm] Unrepost error:', error); + return NextResponse.json({ error: 'Failed to process unrepost' }, { status: 500 }); + } +} diff --git a/src/lib/swarm/interactions.ts b/src/lib/swarm/interactions.ts index 2632050..8fc257c 100644 --- a/src/lib/swarm/interactions.ts +++ b/src/lib/swarm/interactions.ts @@ -110,6 +110,16 @@ export interface SwarmUnfollowPayload { }; } +export interface SwarmUnrepostPayload { + postId: string; + unrepost: { + actorHandle: string; + actorNodeDomain: string; + interactionId: string; + timestamp: string; + }; +} + export interface SwarmMentionPayload { mentionedHandle: string; mention: { @@ -219,6 +229,16 @@ export async function deliverSwarmUnfollow( return deliverSwarmInteraction(targetDomain, '/api/swarm/interactions/unfollow', payload); } +/** + * Deliver an unrepost to a swarm node + */ +export async function deliverSwarmUnrepost( + targetDomain: string, + payload: SwarmUnrepostPayload +): Promise { + return deliverSwarmInteraction(targetDomain, '/api/swarm/interactions/unrepost', payload); +} + /** * Deliver a mention notification to a swarm node */