feat: Enable deletion of swarm-originated posts and replies by propagating requests to remote nodes and improve swarm ID parsing.
This commit is contained in:
@@ -11,8 +11,9 @@ type RouteContext = { params: Promise<{ id: string }> };
|
|||||||
*/
|
*/
|
||||||
function extractSwarmDomain(apId: string | null): string | null {
|
function extractSwarmDomain(apId: string | null): string | null {
|
||||||
if (!apId?.startsWith('swarm:')) return null;
|
if (!apId?.startsWith('swarm:')) return null;
|
||||||
const parts = apId.split(':');
|
const lastColonIndex = apId.lastIndexOf(':');
|
||||||
return parts.length >= 2 ? parts[1] : null;
|
if (lastColonIndex <= 6) return null;
|
||||||
|
return apId.substring(6, lastColonIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,8 +27,10 @@ function isSwarmPost(apId: string | null): boolean {
|
|||||||
* Extract the original post ID from a swarm apId
|
* Extract the original post ID from a swarm apId
|
||||||
*/
|
*/
|
||||||
function extractSwarmPostId(apId: string): string | null {
|
function extractSwarmPostId(apId: string): string | null {
|
||||||
const parts = apId.split(':');
|
if (!apId) return null;
|
||||||
return parts.length >= 3 ? parts[2] : null;
|
const lastColonIndex = apId.lastIndexOf(':');
|
||||||
|
if (lastColonIndex === -1) return null;
|
||||||
|
return apId.substring(lastColonIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like a post
|
// Like a post
|
||||||
@@ -45,15 +48,15 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
// Handle swarm posts (format: swarm:domain:uuid)
|
// Handle swarm posts (format: swarm:domain:uuid)
|
||||||
if (postId.startsWith('swarm:')) {
|
if (postId.startsWith('swarm:')) {
|
||||||
const targetDomain = extractSwarmDomain(postId);
|
const targetDomain = extractSwarmDomain(postId);
|
||||||
const originalPostId = postId.split(':')[2];
|
const originalPostId = extractSwarmPostId(postId);
|
||||||
|
|
||||||
if (!targetDomain || !originalPostId) {
|
if (!targetDomain || !originalPostId) {
|
||||||
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver like directly to the origin node
|
// Deliver like directly to the origin node
|
||||||
const { deliverSwarmLike } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmLike } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmLike(targetDomain, {
|
const result = await deliverSwarmLike(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
like: {
|
like: {
|
||||||
@@ -65,12 +68,12 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(`[Swarm] Like delivery failed: ${result.error}`);
|
console.error(`[Swarm] Like delivery failed: ${result.error}`);
|
||||||
return NextResponse.json({ error: 'Failed to deliver like to remote node' }, { status: 502 });
|
return NextResponse.json({ error: 'Failed to deliver like to remote node' }, { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`);
|
console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`);
|
||||||
return NextResponse.json({ success: true, liked: true });
|
return NextResponse.json({ success: true, liked: true });
|
||||||
}
|
}
|
||||||
@@ -147,12 +150,12 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
if (isSwarmPost(post.apId)) {
|
if (isSwarmPost(post.apId)) {
|
||||||
const targetDomain = extractSwarmDomain(post.apId);
|
const targetDomain = extractSwarmDomain(post.apId);
|
||||||
const originalPostId = extractSwarmPostId(post.apId!);
|
const originalPostId = extractSwarmPostId(post.apId!);
|
||||||
|
|
||||||
if (targetDomain && originalPostId) {
|
if (targetDomain && originalPostId) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const { deliverSwarmLike } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmLike } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmLike(targetDomain, {
|
const result = await deliverSwarmLike(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
like: {
|
like: {
|
||||||
@@ -164,7 +167,7 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`);
|
console.log(`[Swarm] Like delivered to ${targetDomain} for post ${originalPostId}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -223,14 +226,14 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
if (postId.startsWith('swarm:')) {
|
if (postId.startsWith('swarm:')) {
|
||||||
const targetDomain = extractSwarmDomain(postId);
|
const targetDomain = extractSwarmDomain(postId);
|
||||||
const originalPostId = postId.split(':')[2];
|
const originalPostId = postId.split(':')[2];
|
||||||
|
|
||||||
if (!targetDomain || !originalPostId) {
|
if (!targetDomain || !originalPostId) {
|
||||||
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver unlike directly to the origin node
|
// Deliver unlike directly to the origin node
|
||||||
const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmUnlike(targetDomain, {
|
const result = await deliverSwarmUnlike(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
unlike: {
|
unlike: {
|
||||||
@@ -240,12 +243,12 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(`[Swarm] Unlike delivery failed: ${result.error}`);
|
console.error(`[Swarm] Unlike delivery failed: ${result.error}`);
|
||||||
return NextResponse.json({ error: 'Failed to deliver unlike to remote node' }, { status: 502 });
|
return NextResponse.json({ error: 'Failed to deliver unlike to remote node' }, { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Swarm] Unlike delivered to ${targetDomain} for post ${originalPostId}`);
|
console.log(`[Swarm] Unlike delivered to ${targetDomain} for post ${originalPostId}`);
|
||||||
return NextResponse.json({ success: true, liked: false });
|
return NextResponse.json({ success: true, liked: false });
|
||||||
}
|
}
|
||||||
@@ -286,12 +289,12 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
if (isSwarmPost(post.apId)) {
|
if (isSwarmPost(post.apId)) {
|
||||||
const targetDomain = extractSwarmDomain(post.apId);
|
const targetDomain = extractSwarmDomain(post.apId);
|
||||||
const originalPostId = extractSwarmPostId(post.apId!);
|
const originalPostId = extractSwarmPostId(post.apId!);
|
||||||
|
|
||||||
if (targetDomain && originalPostId) {
|
if (targetDomain && originalPostId) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmUnlike } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmUnlike(targetDomain, {
|
const result = await deliverSwarmUnlike(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
unlike: {
|
unlike: {
|
||||||
@@ -301,7 +304,7 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[Swarm] Unlike delivered to ${targetDomain}`);
|
console.log(`[Swarm] Unlike delivered to ${targetDomain}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ type RouteContext = { params: Promise<{ id: string }> };
|
|||||||
*/
|
*/
|
||||||
function extractSwarmDomain(apId: string | null): string | null {
|
function extractSwarmDomain(apId: string | null): string | null {
|
||||||
if (!apId?.startsWith('swarm:')) return null;
|
if (!apId?.startsWith('swarm:')) return null;
|
||||||
const parts = apId.split(':');
|
const lastColonIndex = apId.lastIndexOf(':');
|
||||||
return parts.length >= 2 ? parts[1] : null;
|
if (lastColonIndex <= 6) return null;
|
||||||
|
return apId.substring(6, lastColonIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,8 +27,10 @@ function isSwarmPost(apId: string | null): boolean {
|
|||||||
* Extract the original post ID from a swarm apId
|
* Extract the original post ID from a swarm apId
|
||||||
*/
|
*/
|
||||||
function extractSwarmPostId(apId: string): string | null {
|
function extractSwarmPostId(apId: string): string | null {
|
||||||
const parts = apId.split(':');
|
if (!apId) return null;
|
||||||
return parts.length >= 3 ? parts[2] : null;
|
const lastColonIndex = apId.lastIndexOf(':');
|
||||||
|
if (lastColonIndex === -1) return null;
|
||||||
|
return apId.substring(lastColonIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repost a post
|
// Repost a post
|
||||||
@@ -45,15 +48,15 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
// Handle swarm posts (format: swarm:domain:uuid)
|
// Handle swarm posts (format: swarm:domain:uuid)
|
||||||
if (postId.startsWith('swarm:')) {
|
if (postId.startsWith('swarm:')) {
|
||||||
const targetDomain = extractSwarmDomain(postId);
|
const targetDomain = extractSwarmDomain(postId);
|
||||||
const originalPostId = postId.split(':')[2];
|
const originalPostId = extractSwarmPostId(postId);
|
||||||
|
|
||||||
if (!targetDomain || !originalPostId) {
|
if (!targetDomain || !originalPostId) {
|
||||||
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver repost directly to the origin node
|
// Deliver repost directly to the origin node
|
||||||
const { deliverSwarmRepost } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmRepost } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmRepost(targetDomain, {
|
const result = await deliverSwarmRepost(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
repost: {
|
repost: {
|
||||||
@@ -66,12 +69,12 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(`[Swarm] Repost delivery failed: ${result.error}`);
|
console.error(`[Swarm] Repost delivery failed: ${result.error}`);
|
||||||
return NextResponse.json({ error: 'Failed to deliver repost to remote node' }, { status: 502 });
|
return NextResponse.json({ error: 'Failed to deliver repost to remote node' }, { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Swarm] Repost delivered to ${targetDomain} for post ${originalPostId}`);
|
console.log(`[Swarm] Repost delivered to ${targetDomain} for post ${originalPostId}`);
|
||||||
return NextResponse.json({ success: true, reposted: true });
|
return NextResponse.json({ success: true, reposted: true });
|
||||||
}
|
}
|
||||||
@@ -158,12 +161,12 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
if (isSwarmPost(originalPost.apId)) {
|
if (isSwarmPost(originalPost.apId)) {
|
||||||
const targetDomain = extractSwarmDomain(originalPost.apId);
|
const targetDomain = extractSwarmDomain(originalPost.apId);
|
||||||
const originalPostIdOnRemote = extractSwarmPostId(originalPost.apId!);
|
const originalPostIdOnRemote = extractSwarmPostId(originalPost.apId!);
|
||||||
|
|
||||||
if (targetDomain && originalPostIdOnRemote) {
|
if (targetDomain && originalPostIdOnRemote) {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const { deliverSwarmRepost } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmRepost } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmRepost(targetDomain, {
|
const result = await deliverSwarmRepost(targetDomain, {
|
||||||
postId: originalPostIdOnRemote,
|
postId: originalPostIdOnRemote,
|
||||||
repost: {
|
repost: {
|
||||||
@@ -176,7 +179,7 @@ export async function POST(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
console.log(`[Swarm] Repost delivered to ${targetDomain}`);
|
console.log(`[Swarm] Repost delivered to ${targetDomain}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -241,15 +244,15 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
// Handle swarm posts (format: swarm:domain:uuid)
|
// Handle swarm posts (format: swarm:domain:uuid)
|
||||||
if (postId.startsWith('swarm:')) {
|
if (postId.startsWith('swarm:')) {
|
||||||
const targetDomain = extractSwarmDomain(postId);
|
const targetDomain = extractSwarmDomain(postId);
|
||||||
const originalPostId = postId.split(':')[2];
|
const originalPostId = extractSwarmPostId(postId);
|
||||||
|
|
||||||
if (!targetDomain || !originalPostId) {
|
if (!targetDomain || !originalPostId) {
|
||||||
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid swarm post ID' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deliver unrepost directly to the origin node
|
// Deliver unrepost directly to the origin node
|
||||||
const { deliverSwarmUnrepost } = await import('@/lib/swarm/interactions');
|
const { deliverSwarmUnrepost } = await import('@/lib/swarm/interactions');
|
||||||
|
|
||||||
const result = await deliverSwarmUnrepost(targetDomain, {
|
const result = await deliverSwarmUnrepost(targetDomain, {
|
||||||
postId: originalPostId,
|
postId: originalPostId,
|
||||||
unrepost: {
|
unrepost: {
|
||||||
@@ -259,12 +262,12 @@ export async function DELETE(request: Request, context: RouteContext) {
|
|||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
console.error(`[Swarm] Unrepost delivery failed: ${result.error}`);
|
console.error(`[Swarm] Unrepost delivery failed: ${result.error}`);
|
||||||
return NextResponse.json({ error: 'Failed to deliver unrepost to remote node' }, { status: 502 });
|
return NextResponse.json({ error: 'Failed to deliver unrepost to remote node' }, { status: 502 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Swarm] Unrepost delivered to ${targetDomain} for post ${originalPostId}`);
|
console.log(`[Swarm] Unrepost delivered to ${targetDomain} for post ${originalPostId}`);
|
||||||
return NextResponse.json({ success: true, reposted: false });
|
return NextResponse.json({ success: true, reposted: false });
|
||||||
}
|
}
|
||||||
|
|||||||
+135
-42
@@ -19,11 +19,11 @@ export async function GET(
|
|||||||
|
|
||||||
// Handle swarm post IDs (format: swarm:domain:uuid)
|
// Handle swarm post IDs (format: swarm:domain:uuid)
|
||||||
if (id.startsWith('swarm:')) {
|
if (id.startsWith('swarm:')) {
|
||||||
const parts = id.split(':');
|
const lastColonIndex = id.lastIndexOf(':');
|
||||||
if (parts.length >= 3) {
|
if (lastColonIndex > 6) {
|
||||||
const originDomain = parts[1];
|
const originDomain = id.substring(6, lastColonIndex);
|
||||||
const originalPostId = parts[2];
|
const originalPostId = id.substring(lastColonIndex + 1);
|
||||||
|
|
||||||
// Fetch from origin node in real-time
|
// Fetch from origin node in real-time
|
||||||
try {
|
try {
|
||||||
const protocol = originDomain.includes('localhost') ? 'http' : 'https';
|
const protocol = originDomain.includes('localhost') ? 'http' : 'https';
|
||||||
@@ -31,10 +31,10 @@ export async function GET(
|
|||||||
headers: { 'Accept': 'application/json' },
|
headers: { 'Accept': 'application/json' },
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// Transform to match expected format
|
// Transform to match expected format
|
||||||
mainPost = {
|
mainPost = {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -64,7 +64,7 @@ export async function GET(
|
|||||||
linkPreviewDescription: data.post.linkPreviewDescription,
|
linkPreviewDescription: data.post.linkPreviewDescription,
|
||||||
linkPreviewImage: data.post.linkPreviewImage,
|
linkPreviewImage: data.post.linkPreviewImage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transform replies
|
// Transform replies
|
||||||
replyPosts = (data.replies || []).map((r: any) => ({
|
replyPosts = (data.replies || []).map((r: any) => ({
|
||||||
id: `swarm:${originDomain}:${r.id}`,
|
id: `swarm:${originDomain}:${r.id}`,
|
||||||
@@ -90,17 +90,17 @@ export async function GET(
|
|||||||
altText: m.altText || null,
|
altText: m.altText || null,
|
||||||
})) || [],
|
})) || [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Check if current user has liked this post
|
// Check if current user has liked this post
|
||||||
try {
|
try {
|
||||||
const { requireAuth } = await import('@/lib/auth');
|
const { requireAuth } = await import('@/lib/auth');
|
||||||
const viewer = await requireAuth();
|
const viewer = await requireAuth();
|
||||||
|
|
||||||
const likeCheckRes = await fetch(
|
const likeCheckRes = await fetch(
|
||||||
`${protocol}://${originDomain}/api/swarm/posts/${originalPostId}/likes?checkHandle=${viewer.handle}&checkDomain=${nodeDomain}`,
|
`${protocol}://${originDomain}/api/swarm/posts/${originalPostId}/likes?checkHandle=${viewer.handle}&checkDomain=${nodeDomain}`,
|
||||||
{ signal: AbortSignal.timeout(3000) }
|
{ signal: AbortSignal.timeout(3000) }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (likeCheckRes.ok) {
|
if (likeCheckRes.ok) {
|
||||||
const likeData = await likeCheckRes.json();
|
const likeData = await likeCheckRes.json();
|
||||||
mainPost.isLiked = likeData.isLiked;
|
mainPost.isLiked = likeData.isLiked;
|
||||||
@@ -108,7 +108,7 @@ export async function GET(
|
|||||||
} catch {
|
} catch {
|
||||||
// Not logged in or timeout
|
// Not logged in or timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
post: mainPost,
|
post: mainPost,
|
||||||
replies: replyPosts,
|
replies: replyPosts,
|
||||||
@@ -117,7 +117,7 @@ export async function GET(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[Swarm] Failed to fetch post from ${originDomain}:`, err);
|
console.error(`[Swarm] Failed to fetch post from ${originDomain}:`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,6 +257,100 @@ export async function DELETE(
|
|||||||
|
|
||||||
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
|
||||||
|
|
||||||
|
// Handle swarm post IDs (format: swarm:domain:uuid)
|
||||||
|
if (id.startsWith('swarm:')) {
|
||||||
|
const lastColonIndex = id.lastIndexOf(':');
|
||||||
|
if (lastColonIndex > 6) {
|
||||||
|
const originDomain = id.substring(6, lastColonIndex);
|
||||||
|
const originalPostId = id.substring(lastColonIndex + 1);
|
||||||
|
|
||||||
|
// We need to fetch the post from the remote node to check if the current user is the author
|
||||||
|
// The remote node should have the post with proper attribution
|
||||||
|
try {
|
||||||
|
const protocol = originDomain.includes('localhost') ? 'http' : 'https';
|
||||||
|
const res = await fetch(`${protocol}://${originDomain}/api/swarm/posts/${originalPostId}`, {
|
||||||
|
headers: { 'Accept': 'application/json' },
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
const remotePost = data.post;
|
||||||
|
|
||||||
|
// Check authorship
|
||||||
|
// Format: handle or handle@domain
|
||||||
|
// If the user authored it, the remote post author handle should match current user handle
|
||||||
|
// AND the remote post author node domain should be THIS node
|
||||||
|
|
||||||
|
// The remote node returns author as:
|
||||||
|
// { handle: "user", displayName: "...", nodeDomain: "our-domain" }
|
||||||
|
// OR if it's a "local" user on that node (which shouldn't correspond to us unless we possess that account)
|
||||||
|
|
||||||
|
// In the swarm reply scenario, the remote node stores our user as a "remote user"
|
||||||
|
// Its logic: handle = "user@our-domain"
|
||||||
|
|
||||||
|
// So we check if remotePost.author.handle starts with user.handle
|
||||||
|
// AND (remotePost.author.handle ends with @nodeDomain OR remotePost.nodeDomain == nodeDomain?)
|
||||||
|
|
||||||
|
let isAuthor = false;
|
||||||
|
if (remotePost.author.handle === user.handle ||
|
||||||
|
remotePost.author.handle === `${user.handle}@${nodeDomain}`) {
|
||||||
|
isAuthor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthor) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is our post (or reply). We can delete it.
|
||||||
|
// We need the original ID that WE sent when creating it.
|
||||||
|
// Ideally, the remote node preserves the apId we sent, which contains our original ID.
|
||||||
|
// But the remote node endpoint returns 'apId', let's use that if available.
|
||||||
|
|
||||||
|
let replyIdToDelete = originalPostId; // Fallback to their ID if we can't find ours (unlikely to work for replies endpoint)
|
||||||
|
|
||||||
|
// If we are deleting a reply we sent
|
||||||
|
// The remote node has it stored.
|
||||||
|
// We need to send DELETE /api/swarm/replies with { replyId: <OUR_ID> }
|
||||||
|
// The remote node checks `swarm:ourDomain:<OUR_ID>`
|
||||||
|
|
||||||
|
// We need to extract <OUR_ID> from the remote post's apId
|
||||||
|
// remotePost.apId should be `swarm:ourDomain:ourId`
|
||||||
|
|
||||||
|
if (remotePost.apId && remotePost.apId.startsWith(`swarm:${nodeDomain}:`)) {
|
||||||
|
const parts = remotePost.apId.split(':');
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
replyIdToDelete = parts[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate deletion
|
||||||
|
const deleteRes = await fetch(`${protocol}://${originDomain}/api/swarm/replies`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
replyId: replyIdToDelete,
|
||||||
|
nodeDomain,
|
||||||
|
authorHandle: user.handle,
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(5000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deleteRes.ok) {
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ error: 'Failed to delete on remote node' }, { status: deleteRes.status });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({ error: 'Remote post not found for verification' }, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Swarm] Error deleting remote post:', err);
|
||||||
|
return NextResponse.json({ error: 'Failed to communicate with remote node' }, { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const post = await db.query.posts.findFirst({
|
const post = await db.query.posts.findFirst({
|
||||||
where: eq(posts.id, id),
|
where: eq(posts.id, id),
|
||||||
with: {
|
with: {
|
||||||
@@ -272,7 +366,7 @@ export async function DELETE(
|
|||||||
// OR if user owns the parent post (can delete replies on their posts)
|
// OR if user owns the parent post (can delete replies on their posts)
|
||||||
const isPostOwner = post.userId === user.id;
|
const isPostOwner = post.userId === user.id;
|
||||||
const isBotOwner = post.bot && post.bot.ownerId === user.id;
|
const isBotOwner = post.bot && post.bot.ownerId === user.id;
|
||||||
|
|
||||||
// Check if user owns the parent post (for deleting replies on their posts)
|
// Check if user owns the parent post (for deleting replies on their posts)
|
||||||
let isParentPostOwner = false;
|
let isParentPostOwner = false;
|
||||||
if (post.replyToId) {
|
if (post.replyToId) {
|
||||||
@@ -283,7 +377,7 @@ export async function DELETE(
|
|||||||
isParentPostOwner = true;
|
isParentPostOwner = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isPostOwner && !isBotOwner && !isParentPostOwner) {
|
if (!isPostOwner && !isBotOwner && !isParentPostOwner) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||||
}
|
}
|
||||||
@@ -302,34 +396,33 @@ export async function DELETE(
|
|||||||
|
|
||||||
// 2. If this is a reply to a swarm post, notify the origin node to delete it
|
// 2. If this is a reply to a swarm post, notify the origin node to delete it
|
||||||
if (post.swarmReplyToId) {
|
if (post.swarmReplyToId) {
|
||||||
const parts = post.swarmReplyToId.split(':');
|
// Correctly parse swarm:domain:postId where domain might contain port
|
||||||
if (parts.length >= 3) {
|
const lastColonIndex = post.swarmReplyToId.lastIndexOf(':');
|
||||||
const originDomain = parts[1];
|
if (lastColonIndex > 6) { // 'swarm:'.length = 6
|
||||||
|
const originDomain = post.swarmReplyToId.substring(6, lastColonIndex);
|
||||||
// Fire and forget - don't block on this
|
|
||||||
(async () => {
|
// Propagate deletion to origin node
|
||||||
try {
|
try {
|
||||||
const protocol = originDomain.includes('localhost') ? 'http' : 'https';
|
const protocol = originDomain.includes('localhost') ? 'http' : 'https';
|
||||||
const res = await fetch(`${protocol}://${originDomain}/api/swarm/replies`, {
|
const res = await fetch(`${protocol}://${originDomain}/api/swarm/replies`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
replyId: post.id,
|
replyId: post.id,
|
||||||
nodeDomain,
|
nodeDomain,
|
||||||
authorHandle: user.handle,
|
authorHandle: user.handle,
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(5000),
|
signal: AbortSignal.timeout(5000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
console.log(`[Swarm] Deletion propagated to ${originDomain}`);
|
console.log(`[Swarm] Deletion propagated to ${originDomain}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`[Swarm] Failed to propagate deletion: ${res.status}`);
|
console.error(`[Swarm] Failed to propagate deletion: ${res.status}`);
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[Swarm] Error propagating deletion:', err);
|
|
||||||
}
|
}
|
||||||
})();
|
} catch (err) {
|
||||||
|
console.error('[Swarm] Error propagating deletion:', err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,11 @@ export async function GET(request: NextRequest, context: RouteContext) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const author = post.author as any;
|
const author = post.author as any;
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
post: {
|
post: {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
|
apId: post.apId, // Expose apId for swarm coordination (e.g. deletion recovery)
|
||||||
content: post.content,
|
content: post.content,
|
||||||
createdAt: post.createdAt.toISOString(),
|
createdAt: post.createdAt.toISOString(),
|
||||||
likesCount: post.likesCount,
|
likesCount: post.likesCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user