refactor: Migrate from ActivityPub federation to native Swarm network

- Remove ActivityPub infrastructure (webfinger, nodeinfo, inbox, activities, signatures)
- Implement native peer-to-peer Swarm network with gossip protocol
- Replace federated timeline with Swarm timeline aggregating posts from all nodes
- Migrate direct messaging to end-to-end encrypted Swarm Chat system
- Update user interactions (follow, like, repost) to use Swarm protocol
- Add cryptographic key management for DID-based identity system
- Remove server-bound identity model in favor of portable DID identities
- Update documentation to reflect Swarm architecture and capabilities
- Add debug utilities and chat testing tools for Swarm network
- Refactor database schema to support distributed user directory
- Update bot framework to work with Swarm network interactions
- Simplify API routes to use Swarm protocol instead of ActivityPub
- This transition enables true peer-to-peer communication, instant interactions, and encrypted messaging while maintaining sovereign identity through DIDs
This commit is contained in:
Christomatt
2026-01-27 01:27:15 +01:00
parent 6b8eeb6814
commit eb63194c56
49 changed files with 1173 additions and 3559 deletions
+6 -6
View File
@@ -7,7 +7,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, chatConversations, chatMessages, users } from '@/db';
import { eq, desc, and, lt } from 'drizzle-orm';
import { eq, desc, and, lt, isNull } from 'drizzle-orm';
import { getSession } from '@/lib/auth';
import { decryptMessage } from '@/lib/swarm/chat-crypto';
@@ -53,10 +53,10 @@ export async function GET(request: NextRequest) {
}
// Build query with cursor-based pagination
let whereCondition = eq(chatMessages.conversationId, conversationId);
if (cursor) {
whereCondition = and(whereCondition, lt(chatMessages.createdAt, new Date(cursor)));
}
const baseCondition = eq(chatMessages.conversationId, conversationId);
const whereCondition = cursor
? and(baseCondition, lt(chatMessages.createdAt, new Date(cursor)))!
: baseCondition;
// Get messages
const messages = await db.query.chatMessages.findMany({
@@ -124,7 +124,7 @@ export async function PATCH(request: NextRequest) {
.where(
and(
eq(chatMessages.conversationId, conversationId),
eq(chatMessages.readAt, null)
isNull(chatMessages.readAt)
)
);
+55 -9
View File
@@ -18,18 +18,35 @@ const sendMessageSchema = z.object({
});
export async function POST(request: NextRequest) {
console.log('[Chat Send] Starting request processing');
try {
if (!db) {
console.error('[Chat Send] Database connection missing');
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
}
const session = await getSession();
if (!session?.user) {
console.warn('[Chat Send] Unauthorized attempt');
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
console.log('[Chat Send] User authenticated:', session.user.id);
const body = await request.json();
const data = sendMessageSchema.parse(body);
let body;
try {
body = await request.json();
} catch (e) {
console.error('[Chat Send] Failed to parse JSON body');
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
const parseResult = sendMessageSchema.safeParse(body);
if (!parseResult.success) {
console.error('[Chat Send] Schema validation failed:', parseResult.error);
return NextResponse.json({ error: 'Invalid input', details: parseResult.error.issues }, { status: 400 });
}
const data = parseResult.data;
console.log('[Chat Send] Input validated. Recipient:', data.recipientHandle);
// Get sender info
const sender = await db.query.users.findFirst({
@@ -37,13 +54,15 @@ export async function POST(request: NextRequest) {
});
if (!sender) {
console.error('[Chat Send] Sender not found in DB:', session.user.id);
return NextResponse.json({ error: 'Sender not found' }, { status: 404 });
}
console.log('[Chat Send] Sender retrieved:', sender.handle);
// Parse recipient handle (could be local or remote)
const recipientHandle = data.recipientHandle.toLowerCase();
const isRemote = recipientHandle.includes('@');
let recipientUser: typeof users.$inferSelect | undefined;
let recipientPublicKey: string;
let recipientNodeDomain: string | null = null;
@@ -52,6 +71,7 @@ export async function POST(request: NextRequest) {
// Remote user - need to fetch their public key
const [handle, domain] = recipientHandle.split('@');
recipientNodeDomain = domain;
console.log('[Chat Send] Processing remote recipient:', handle, '@', domain);
// Try to find cached remote user
recipientUser = await db.query.users.findFirst({
@@ -61,10 +81,12 @@ export async function POST(request: NextRequest) {
if (!recipientUser) {
// Fetch from remote node
try {
console.log('[Chat Send] Fetching remote user from node:', domain);
const protocol = domain.includes('localhost') ? 'http' : 'https';
const response = await fetch(`${protocol}://${domain}/api/users/${handle}`);
if (!response.ok) {
console.error('[Chat Send] Remote user fetch failed. Status:', response.status);
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
}
@@ -80,24 +102,29 @@ export async function POST(request: NextRequest) {
publicKey: recipientPublicKey,
}).returning();
recipientUser = newUser;
console.log('[Chat Send] Remote user cached');
} catch (error) {
console.error('Failed to fetch remote user:', error);
console.error('[Chat Send] Failed to fetch remote user:', error);
return NextResponse.json({ error: 'Failed to reach recipient node' }, { status: 503 });
}
} else {
recipientPublicKey = recipientUser.publicKey;
console.log('[Chat Send] Remote user found in cache');
}
} else {
// Local user
console.log('[Chat Send] Processing local recipient');
recipientUser = await db.query.users.findFirst({
where: eq(users.handle, recipientHandle),
});
if (!recipientUser) {
console.warn('[Chat Send] Local recipient not found:', recipientHandle);
return NextResponse.json({ error: 'Recipient not found' }, { status: 404 });
}
if (recipientUser.isSuspended) {
console.warn('[Chat Send] Local recipient suspended:', recipientHandle);
return NextResponse.json({ error: 'Recipient not available' }, { status: 404 });
}
@@ -105,7 +132,14 @@ export async function POST(request: NextRequest) {
}
// Encrypt the message with recipient's public key
const encryptedContent = encryptMessage(data.content, recipientPublicKey);
console.log('[Chat Send] Encrypting message...');
let encryptedContent: string;
try {
encryptedContent = encryptMessage(data.content, recipientPublicKey);
} catch (encError) {
console.error('[Chat Send] Encryption failed:', encError);
return NextResponse.json({ error: 'Encryption failed' }, { status: 500 });
}
// Get or create conversation
let conversation = await db.query.chatConversations.findFirst({
@@ -116,6 +150,7 @@ export async function POST(request: NextRequest) {
});
if (!conversation) {
console.log('[Chat Send] Creating new conversation');
const [newConversation] = await db.insert(chatConversations).values({
participant1Id: sender.id,
participant2Handle: recipientHandle,
@@ -130,6 +165,7 @@ export async function POST(request: NextRequest) {
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
const swarmMessageId = `swarm:${nodeDomain}:${messageId}`;
console.log('[Chat Send] Inserting message into DB');
const [newMessage] = await db.insert(chatMessages).values({
conversationId: conversation.id,
senderHandle: sender.handle,
@@ -153,6 +189,11 @@ export async function POST(request: NextRequest) {
// If remote, send to their node
if (isRemote && recipientNodeDomain) {
// ... (remote logic remains similar but add logs)
console.log('[Chat Send] Dispatching to remote node:', recipientNodeDomain);
// ... existing remote send logic ...
// For brevity in this tool call, I'm keeping the original logic mostly intact but wrapped/logged.
// Re-implementing the block:
try {
const payload: SwarmChatMessagePayload = {
messageId,
@@ -177,22 +218,27 @@ export async function POST(request: NextRequest) {
await db.update(chatMessages)
.set({ deliveredAt: new Date() })
.where(eq(chatMessages.id, newMessage.id));
console.log('[Chat Send] Remote delivery confirmed');
} else {
console.warn('[Chat Send] Remote delivery failed. Status:', response.status);
}
} catch (error) {
console.error('Failed to send message to remote node:', error);
console.error('[Chat Send] Failed to send message to remote node:', error);
// Message is still stored locally, will show as undelivered
}
}
console.log('[Chat Send] Success');
return NextResponse.json({
success: true,
message: newMessage,
});
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'Invalid input', details: error.errors }, { status: 400 });
// Should be caught by safeParse above, but just in case
return NextResponse.json({ error: 'Invalid input', details: error.issues }, { status: 400 });
}
console.error('Send message error:', error);
console.error('[Chat Send] Unhandled error:', error);
return NextResponse.json({ error: 'Failed to send message' }, { status: 500 });
}
}
+2 -2
View File
@@ -3,8 +3,8 @@
*
* POST: Receive posts from users on other swarm nodes that local users follow
*
* This is the swarm equivalent of ActivityPub inbox - when a user on another
* Synapsis node creates a post, it gets pushed here for their followers.
* When a user on another Synapsis node creates a post, it gets pushed here
* for their followers on this node.
*/
import { NextRequest, NextResponse } from 'next/server';
@@ -3,8 +3,8 @@
*
* POST: Receive a follow from another swarm node
*
* This enables swarm-native follows between Synapsis nodes,
* bypassing ActivityPub for faster, more direct connections.
* This enables swarm-native follows between Synapsis nodes
* with instant delivery and real-time updates.
*/
import { NextRequest, NextResponse } from 'next/server';
+5 -3
View File
@@ -60,10 +60,11 @@ export async function GET(request: NextRequest) {
// Use query builder for better conditional logic
// Only return posts from local users (not remote placeholder users)
// Local posts may have apId if they've been federated, so we check nodeId instead
let whereCondition = and(
isNull(posts.replyToId), // Not a reply
eq(posts.isRemoved, false), // Not removed
isNull(posts.apId) // Not a federated/swarm post (local posts only)
isNull(users.nodeId) // Local user (not from another swarm node)
);
if (cursor) {
@@ -77,7 +78,6 @@ export async function GET(request: NextRequest) {
}
// Get recent public posts (not replies, local users only, not removed)
// Filter out remote placeholder users by checking handle doesn't contain @
const recentPosts = await db
.select({
id: posts.id,
@@ -100,10 +100,12 @@ export async function GET(request: NextRequest) {
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(and(whereCondition, sql`${users.handle} NOT LIKE '%@%'`))
.where(whereCondition)
.orderBy(desc(posts.createdAt))
.limit(limit);
console.log(`[Swarm Timeline API] Found ${recentPosts.length} posts for ${nodeDomain}`);
// Fetch media for each post
const swarmPosts: SwarmPost[] = [];