ceae76d58f
Removed all E2EE chat endpoints, crypto logic, and related API routes, transitioning chat to plain text storage and transport. Updated chat send/receive endpoints to use signed actions and enforce DM privacy settings. Cleaned up Swarm chat inbox, key management, and related debug endpoints. Updated user profile to support DM privacy, improved search for handle queries, and refactored conversation/message logic to support the new model. Migrated bot settings and privacy settings to new locations.
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
/**
|
|
* Swarm User Profile Endpoint
|
|
*
|
|
* GET: Returns a user's profile and posts for swarm requests
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db, posts, users, media, nodes } from '@/db';
|
|
import { eq, desc, and } from 'drizzle-orm';
|
|
|
|
export interface SwarmUserProfile {
|
|
handle: string;
|
|
displayName: string;
|
|
bio?: string;
|
|
avatarUrl?: string;
|
|
headerUrl?: string;
|
|
website?: string;
|
|
followersCount: number;
|
|
followingCount: number;
|
|
postsCount: number;
|
|
createdAt: string;
|
|
isBot?: boolean;
|
|
botOwnerHandle?: string; // Handle of the bot's owner (e.g., "user" or "user@domain")
|
|
nodeDomain: string;
|
|
publicKey?: string; // Signing key for verifying actions
|
|
did?: string;
|
|
}
|
|
|
|
export interface SwarmUserPost {
|
|
id: string;
|
|
content: string;
|
|
createdAt: string;
|
|
isNsfw: boolean;
|
|
likesCount: number;
|
|
repostsCount: number;
|
|
repliesCount: number;
|
|
media?: { url: string; mimeType?: string; altText?: string }[];
|
|
linkPreviewUrl?: string;
|
|
linkPreviewTitle?: string;
|
|
linkPreviewDescription?: string;
|
|
linkPreviewImage?: string;
|
|
}
|
|
|
|
type RouteContext = { params: Promise<{ handle: string }> };
|
|
|
|
/**
|
|
* GET /api/swarm/users/[handle]
|
|
*
|
|
* Returns a user's profile and recent posts.
|
|
* Used by other nodes to display remote user profiles.
|
|
*/
|
|
export async function GET(request: NextRequest, context: RouteContext) {
|
|
try {
|
|
const { handle } = await context.params;
|
|
const cleanHandle = handle.toLowerCase().replace(/^@/, '');
|
|
const { searchParams } = new URL(request.url);
|
|
const limit = Math.min(parseInt(searchParams.get('limit') || '25'), 50);
|
|
|
|
if (!db) {
|
|
return NextResponse.json({ error: 'Database not available' }, { status: 503 });
|
|
}
|
|
|
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
|
|
|
|
// Find the user
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.handle, cleanHandle),
|
|
with: {
|
|
botOwner: true, // Include bot owner if this is a bot
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
if (user.isSuspended) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
// Build profile response
|
|
const profile: SwarmUserProfile = {
|
|
handle: user.handle,
|
|
displayName: user.displayName || user.handle,
|
|
bio: user.bio || undefined,
|
|
avatarUrl: user.avatarUrl || undefined,
|
|
headerUrl: user.headerUrl || undefined,
|
|
website: user.website || undefined,
|
|
followersCount: user.followersCount,
|
|
followingCount: user.followingCount,
|
|
postsCount: user.postsCount,
|
|
createdAt: user.createdAt.toISOString(),
|
|
isBot: user.isBot || undefined,
|
|
botOwnerHandle: user.isBot && user.botOwner ? user.botOwner.handle : undefined,
|
|
nodeDomain,
|
|
publicKey: user.publicKey, // Expose signing key
|
|
did: user.did || undefined,
|
|
};
|
|
|
|
// Get user's recent posts
|
|
const userPosts = await db
|
|
.select({
|
|
id: posts.id,
|
|
content: posts.content,
|
|
createdAt: posts.createdAt,
|
|
isNsfw: posts.isNsfw,
|
|
likesCount: posts.likesCount,
|
|
repostsCount: posts.repostsCount,
|
|
repliesCount: posts.repliesCount,
|
|
linkPreviewUrl: posts.linkPreviewUrl,
|
|
linkPreviewTitle: posts.linkPreviewTitle,
|
|
linkPreviewDescription: posts.linkPreviewDescription,
|
|
linkPreviewImage: posts.linkPreviewImage,
|
|
})
|
|
.from(posts)
|
|
.where(
|
|
and(
|
|
eq(posts.userId, user.id),
|
|
eq(posts.isRemoved, false)
|
|
)
|
|
)
|
|
.orderBy(desc(posts.createdAt))
|
|
.limit(limit);
|
|
|
|
// 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 })
|
|
.from(media)
|
|
.where(eq(media.postId, post.id));
|
|
|
|
swarmPosts.push({
|
|
id: post.id,
|
|
content: post.content,
|
|
createdAt: post.createdAt.toISOString(),
|
|
isNsfw: post.isNsfw,
|
|
likesCount: post.likesCount,
|
|
repostsCount: post.repostsCount,
|
|
repliesCount: post.repliesCount,
|
|
media: postMedia.length > 0 ? postMedia.map(m => ({
|
|
url: m.url,
|
|
mimeType: m.mimeType || undefined,
|
|
altText: m.altText || undefined,
|
|
})) : undefined,
|
|
linkPreviewUrl: post.linkPreviewUrl || undefined,
|
|
linkPreviewTitle: post.linkPreviewTitle || undefined,
|
|
linkPreviewDescription: post.linkPreviewDescription || undefined,
|
|
linkPreviewImage: post.linkPreviewImage || undefined,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
profile,
|
|
posts: swarmPosts,
|
|
nodeDomain,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
} catch (error) {
|
|
console.error('Swarm user profile error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch user profile' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|