diff --git a/src/app/api/swarm/users/[handle]/route.ts b/src/app/api/swarm/users/[handle]/route.ts index 0732486..171f220 100644 --- a/src/app/api/swarm/users/[handle]/route.ts +++ b/src/app/api/swarm/users/[handle]/route.ts @@ -22,6 +22,7 @@ export interface SwarmUserProfile { isBot?: boolean; botOwnerHandle?: string; // Handle of the bot's owner (e.g., "user" or "user@domain") nodeDomain: string; + chatPublicKey?: string; } export interface SwarmUserPost { @@ -91,6 +92,7 @@ export async function GET(request: NextRequest, context: RouteContext) { isBot: user.isBot || undefined, botOwnerHandle: user.isBot && user.botOwner ? user.botOwner.handle : undefined, nodeDomain, + chatPublicKey: user.chatPublicKey || undefined, }; // Get user's recent posts @@ -120,7 +122,7 @@ export async function GET(request: NextRequest, context: RouteContext) { // Fetch media for each post const swarmPosts: SwarmUserPost[] = []; - + for (const post of userPosts) { const postMedia = await db .select({ url: media.url, mimeType: media.mimeType, altText: media.altText }) diff --git a/src/app/api/users/[handle]/route.ts b/src/app/api/users/[handle]/route.ts index 17131fa..3a44de6 100644 --- a/src/app/api/users/[handle]/route.ts +++ b/src/app/api/users/[handle]/route.ts @@ -35,7 +35,7 @@ export async function GET(request: Request, context: RouteContext) { // If user exists but is a remote placeholder (handle contains @), fetch fresh data from remote const isRemotePlaceholder = user && cleanHandle.includes('@'); - + if (!user || isRemotePlaceholder) { if (remoteHandle && remoteDomain) { // Only fetch from swarm nodes @@ -44,7 +44,7 @@ export async function GET(request: Request, context: RouteContext) { const profileData = await fetchSwarmUserProfile(remoteHandle, remoteDomain, 0); if (profileData?.profile) { const profile = profileData.profile; - + return NextResponse.json({ user: { id: `swarm:${remoteDomain}:${profile.handle}`, @@ -62,11 +62,12 @@ export async function GET(request: Request, context: RouteContext) { isSwarm: true, nodeDomain: remoteDomain, isBot: profile.isBot || false, + chatPublicKey: profile.chatPublicKey, } }); } } - + // Non-swarm nodes are no longer supported return NextResponse.json({ error: 'User not found. Only Synapsis swarm nodes are supported.' }, { status: 404 }); } @@ -97,7 +98,7 @@ export async function GET(request: Request, context: RouteContext) { publicKey: user.publicKey, // RSA key for signing chatPublicKey: user.chatPublicKey, // ECDH key for E2E chat }; - + // If this is a bot, include owner info if (user.isBot && user.botOwnerId) { const owner = await db.query.users.findFirst({ @@ -112,7 +113,7 @@ export async function GET(request: Request, context: RouteContext) { }; } } - + return NextResponse.json({ user: userResponse }); } catch (error) { console.error('Get user error:', error); diff --git a/src/lib/swarm/interactions.ts b/src/lib/swarm/interactions.ts index 0095f69..41c4c19 100644 --- a/src/lib/swarm/interactions.ts +++ b/src/lib/swarm/interactions.ts @@ -257,17 +257,17 @@ async function deliverSwarmInteraction( payload: unknown ): Promise { try { - const baseUrl = targetDomain.startsWith('http') - ? targetDomain + const baseUrl = targetDomain.startsWith('http') + ? targetDomain : targetDomain.startsWith('localhost') || targetDomain.startsWith('127.0.0.1') ? `http://${targetDomain}` : `https://${targetDomain}`; - + const url = `${baseUrl}${endpoint}`; - + const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout - + const response = await fetch(url, { method: 'POST', headers: { @@ -277,9 +277,9 @@ async function deliverSwarmInteraction( body: JSON.stringify(payload), signal: controller.signal, }); - + clearTimeout(timeout); - + if (!response.ok) { const errorText = await response.text().catch(() => 'Unknown error'); return { @@ -287,7 +287,7 @@ async function deliverSwarmInteraction( error: `HTTP ${response.status}: ${errorText}`, }; } - + const data = await response.json(); return { success: true, @@ -318,7 +318,9 @@ export interface SwarmUserProfile { postsCount: number; createdAt: string; isBot?: boolean; + botOwnerHandle?: string; // Handle of the bot's owner (e.g., "user" or "user@domain") nodeDomain: string; + chatPublicKey?: string; } export interface SwarmUserPost { @@ -357,23 +359,23 @@ export async function fetchSwarmUserProfile( : domain.startsWith('localhost') || domain.startsWith('127.0.0.1') ? `http://${domain}` : `https://${domain}`; - + const url = `${baseUrl}/api/swarm/users/${handle}?limit=${postsLimit}`; - + const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); - + const response = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal, }); - + clearTimeout(timeout); - + if (!response.ok) { return null; } - + return await response.json(); } catch (error) { console.error(`[Swarm] Failed to fetch profile for ${handle}@${domain}:`, error); @@ -393,7 +395,7 @@ export async function cacheSwarmUserPosts( ): Promise<{ cached: number; skipped: number }> { try { const profileData = await fetchSwarmUserProfile(handle, domain, limit); - + if (!profileData || !profileData.posts) { return { cached: 0, skipped: 0 }; } @@ -464,23 +466,23 @@ export async function fetchSwarmPost( : domain.startsWith('localhost') || domain.startsWith('127.0.0.1') ? `http://${domain}` : `https://${domain}`; - + const url = `${baseUrl}/api/swarm/posts/${postId}`; - + const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); - + const response = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal, }); - + clearTimeout(timeout); - + if (!response.ok) { return null; } - + return await response.json(); } catch (error) { console.error(`[Swarm] Failed to fetch post ${postId} from ${domain}:`, error); @@ -500,7 +502,7 @@ export function extractMentions(content: string): { handle: string; domain: stri // Match @handle or @handle@domain patterns const mentionRegex = /@([a-zA-Z0-9_]+)(?:@([a-zA-Z0-9.-]+))?/g; const mentions: { handle: string; domain: string | null }[] = []; - + let match; while ((match = mentionRegex.exec(content)) !== null) { mentions.push({ @@ -508,7 +510,7 @@ export function extractMentions(content: string): { handle: string; domain: stri domain: match[2]?.toLowerCase() || null, }); } - + return mentions; } @@ -528,15 +530,15 @@ export async function deliverSwarmMentions( const mentions = extractMentions(content); let delivered = 0; let failed = 0; - + for (const mention of mentions) { // Skip local mentions (no domain) if (!mention.domain) continue; - + // Check if it's a swarm node const isSwarm = await isSwarmNode(mention.domain); if (!isSwarm) continue; - + // Deliver the mention const result = await deliverSwarmMention(mention.domain, { mentionedHandle: mention.handle, @@ -551,14 +553,14 @@ export async function deliverSwarmMentions( timestamp: new Date().toISOString(), }, }); - + if (result.success) { delivered++; } else { failed++; } } - + return { delivered, failed }; } @@ -619,7 +621,7 @@ export async function getSwarmFollowerDomains(userId: string): Promise // Filter for swarm followers (actorUrl starts with swarm://) const swarmFollowers = followers.filter(f => f.actorUrl.startsWith('swarm://')); - + // Extract unique domains const domains = swarmFollowers.map(f => { const match = f.actorUrl.match(/^swarm:\/\/([^\/]+)/); @@ -660,7 +662,7 @@ export async function deliverPostToSwarmFollowers( nodeDomain: string ): Promise<{ delivered: number; failed: number }> { const swarmDomains = await getSwarmFollowerDomains(userId); - + if (swarmDomains.length === 0) { return { delivered: 0, failed: 0 }; }