Implement federated chat conversation deletion
Adds logic to notify remote nodes when a chat conversation is deleted, and introduces a new API endpoint to receive and process deletion requests from other swarm nodes. Ensures that only authorized participants can trigger deletions and handles both local and remote user scenarios.
This commit is contained in:
@@ -43,8 +43,57 @@ export async function DELETE(
|
|||||||
// Delete the entire conversation and all messages (cascade will handle messages)
|
// Delete the entire conversation and all messages (cascade will handle messages)
|
||||||
await db.delete(chatConversations).where(eq(chatConversations.id, id));
|
await db.delete(chatConversations).where(eq(chatConversations.id, id));
|
||||||
|
|
||||||
// TODO: Send a federation message to the other party to delete their copy
|
// Send deletion request to the other party
|
||||||
// This would require implementing a swarm protocol for conversation deletion
|
const participant2Handle = conversation.participant2Handle;
|
||||||
|
const isRemote = participant2Handle.includes('@');
|
||||||
|
|
||||||
|
if (isRemote) {
|
||||||
|
// Extract domain from handle (format: handle@domain)
|
||||||
|
const domain = participant2Handle.split('@')[1];
|
||||||
|
const handle = participant2Handle.split('@')[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
||||||
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
|
||||||
|
|
||||||
|
await fetch(`${protocol}://${domain}/api/swarm/chat/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
senderHandle: session.user.handle,
|
||||||
|
senderNodeDomain: nodeDomain,
|
||||||
|
recipientHandle: handle,
|
||||||
|
conversationId: id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[Chat Delete] Sent deletion request to ${domain}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Chat Delete] Failed to notify remote node:', error);
|
||||||
|
// Continue anyway - local deletion succeeded
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Local user - find and delete their conversation too
|
||||||
|
const recipientUser = await db.query.users.findFirst({
|
||||||
|
where: eq(db.users.handle, participant2Handle),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recipientUser) {
|
||||||
|
// Find their conversation with us
|
||||||
|
const recipientConversation = await db.query.chatConversations.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(chatConversations.participant1Id, recipientUser.id),
|
||||||
|
eq(chatConversations.participant2Handle, session.user.handle)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (recipientConversation) {
|
||||||
|
await db.delete(chatConversations).where(eq(chatConversations.id, recipientConversation.id));
|
||||||
|
console.log(`[Chat Delete] Deleted conversation for local user ${participant2Handle}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Swarm Chat Deletion Inbox
|
||||||
|
*
|
||||||
|
* POST: Receives conversation deletion requests from other swarm nodes
|
||||||
|
*
|
||||||
|
* Security: Only allows deletion if the sender is actually a participant in the conversation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { db, users, chatConversations } from '@/db';
|
||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const deletionSchema = z.object({
|
||||||
|
senderHandle: z.string(),
|
||||||
|
senderNodeDomain: z.string(),
|
||||||
|
recipientHandle: z.string(),
|
||||||
|
conversationId: z.string().optional(),
|
||||||
|
timestamp: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
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 = deletionSchema.parse(body);
|
||||||
|
|
||||||
|
// Find the recipient (local user)
|
||||||
|
const recipient = await db.query.users.findFirst({
|
||||||
|
where: eq(users.handle, data.recipientHandle.toLowerCase()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the conversation with the sender
|
||||||
|
const senderFullHandle = `${data.senderHandle}@${data.senderNodeDomain}`;
|
||||||
|
const conversation = await db.query.chatConversations.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(chatConversations.participant1Id, recipient.id),
|
||||||
|
eq(chatConversations.participant2Handle, senderFullHandle)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!conversation) {
|
||||||
|
// Conversation doesn't exist - could be already deleted or never existed
|
||||||
|
// Return success to avoid leaking information about conversation existence
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Conversation not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY CHECK: Verify the sender is actually a participant in this conversation
|
||||||
|
// The conversation must be between the recipient (participant1) and the sender (participant2)
|
||||||
|
if (conversation.participant2Handle !== senderFullHandle) {
|
||||||
|
console.warn(`[Swarm Chat Delete] Unauthorized deletion attempt from ${senderFullHandle} for conversation with ${conversation.participant2Handle}`);
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the conversation (cascade will delete messages)
|
||||||
|
await db.delete(chatConversations).where(eq(chatConversations.id, conversation.id));
|
||||||
|
|
||||||
|
console.log(`[Swarm Chat Delete] Deleted conversation between ${recipient.handle} and ${senderFullHandle}`);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Conversation deleted',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
return NextResponse.json({ error: 'Invalid payload', details: error.issues }, { status: 400 });
|
||||||
|
}
|
||||||
|
console.error('Swarm chat deletion error:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to process deletion' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user