feat: include chatPublicKey in user profiles and the swarm user interface.

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