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;
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 })
+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
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);
+32 -30
View File
@@ -257,17 +257,17 @@ async function deliverSwarmInteraction(
payload: unknown
): Promise<SwarmInteractionResponse> {
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<string[]>
// 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 };
}