166 lines
4.9 KiB
TypeScript
166 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;
|
|
chatPublicKey?: 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,
|
|
chatPublicKey: user.chatPublicKey || 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 }
|
|
);
|
|
}
|
|
}
|