0ecb622565
Hop-State: A_06FP6A2CG8PVBRDVQ5WGCN0 Hop-Proposal: R_06FP6A0YW23RY0N7Z5M8DW0 Hop-Task: T_06FP67JVF4BHPYKKC37XKQ8 Hop-Attempt: AT_06FP69RZ96ZMNF9Y5S02MT0
258 lines
8.0 KiB
TypeScript
258 lines
8.0 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, userSwarmReposts } from '@/db';
|
|
import { eq, desc, and, isNull } from 'drizzle-orm';
|
|
import { parseLinkPreviewMediaJson } from '@/lib/media/linkPreview';
|
|
|
|
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;
|
|
originalPostId?: string;
|
|
content: string;
|
|
createdAt: string;
|
|
isNsfw: boolean;
|
|
likesCount: number;
|
|
repostsCount: number;
|
|
repliesCount: number;
|
|
nodeDomain?: string;
|
|
author?: {
|
|
handle: string;
|
|
displayName?: string;
|
|
avatarUrl?: string;
|
|
isBot?: boolean;
|
|
nodeDomain?: string;
|
|
};
|
|
media?: { url: string; mimeType?: string; altText?: string }[];
|
|
linkPreviewUrl?: string;
|
|
linkPreviewTitle?: string;
|
|
linkPreviewDescription?: string;
|
|
linkPreviewImage?: string;
|
|
linkPreviewType?: 'card' | 'image' | 'gallery' | 'video';
|
|
linkPreviewVideoUrl?: string;
|
|
linkPreviewMedia?: Array<{ url: string; width?: number | null; height?: number | null; mimeType?: string | null }>;
|
|
repostOfId?: string;
|
|
repostOf?: SwarmUserPost | null;
|
|
}
|
|
|
|
type RouteContext = { params: Promise<{ handle: string }> };
|
|
|
|
const profilePostRelations = {
|
|
author: true,
|
|
media: true,
|
|
repostOf: {
|
|
with: {
|
|
author: true,
|
|
media: true,
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
function parseMediaJson(mediaJson: string | null) {
|
|
if (!mediaJson) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(mediaJson);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function mapLocalPostToSwarmPost(post: any, nodeDomain: string): SwarmUserPost {
|
|
return {
|
|
id: post.id,
|
|
originalPostId: post.id,
|
|
content: post.content,
|
|
createdAt: post.createdAt.toISOString(),
|
|
isNsfw: post.isNsfw,
|
|
likesCount: post.likesCount,
|
|
repostsCount: post.repostsCount,
|
|
repliesCount: post.repliesCount,
|
|
nodeDomain,
|
|
author: post.author ? {
|
|
handle: post.author.handle,
|
|
displayName: post.author.displayName || post.author.handle,
|
|
avatarUrl: post.author.avatarUrl || undefined,
|
|
isBot: post.author.isBot || undefined,
|
|
nodeDomain,
|
|
} : undefined,
|
|
media: (post.media || []).map((item: any) => ({
|
|
url: item.url,
|
|
mimeType: item.mimeType || undefined,
|
|
altText: item.altText || undefined,
|
|
})),
|
|
linkPreviewUrl: post.linkPreviewUrl || undefined,
|
|
linkPreviewTitle: post.linkPreviewTitle || undefined,
|
|
linkPreviewDescription: post.linkPreviewDescription || undefined,
|
|
linkPreviewImage: post.linkPreviewImage || undefined,
|
|
linkPreviewType: post.linkPreviewType || undefined,
|
|
linkPreviewVideoUrl: post.linkPreviewVideoUrl || undefined,
|
|
linkPreviewMedia: parseLinkPreviewMediaJson(post.linkPreviewMediaJson),
|
|
repostOfId: post.repostOfId || undefined,
|
|
repostOf: post.repostOf ? mapLocalPostToSwarmPost(post.repostOf, nodeDomain) : undefined,
|
|
};
|
|
}
|
|
|
|
function mapUserSwarmRepostToSwarmPost(
|
|
row: typeof userSwarmReposts.$inferSelect,
|
|
author: typeof users.$inferSelect,
|
|
nodeDomain: string
|
|
): SwarmUserPost {
|
|
return {
|
|
id: row.id,
|
|
originalPostId: row.id,
|
|
content: '',
|
|
createdAt: row.repostedAt.toISOString(),
|
|
isNsfw: false,
|
|
likesCount: 0,
|
|
repostsCount: 0,
|
|
repliesCount: 0,
|
|
nodeDomain,
|
|
author: {
|
|
handle: author.handle,
|
|
displayName: author.displayName || author.handle,
|
|
avatarUrl: author.avatarUrl || undefined,
|
|
isBot: author.isBot || undefined,
|
|
nodeDomain,
|
|
},
|
|
repostOfId: row.originalPostId,
|
|
repostOf: {
|
|
id: row.originalPostId,
|
|
originalPostId: row.originalPostId,
|
|
content: row.content,
|
|
createdAt: row.postCreatedAt.toISOString(),
|
|
isNsfw: false,
|
|
likesCount: row.likesCount,
|
|
repostsCount: row.repostsCount,
|
|
repliesCount: row.repliesCount,
|
|
nodeDomain: row.nodeDomain,
|
|
author: {
|
|
handle: row.authorHandle.includes('@') ? row.authorHandle : `${row.authorHandle}@${row.nodeDomain}`,
|
|
displayName: row.authorDisplayName || row.authorHandle,
|
|
avatarUrl: row.authorAvatarUrl || undefined,
|
|
nodeDomain: row.nodeDomain,
|
|
},
|
|
media: parseMediaJson(row.mediaJson),
|
|
linkPreviewUrl: row.linkPreviewUrl || undefined,
|
|
linkPreviewTitle: row.linkPreviewTitle || undefined,
|
|
linkPreviewDescription: row.linkPreviewDescription || undefined,
|
|
linkPreviewImage: row.linkPreviewImage || undefined,
|
|
linkPreviewType: (row.linkPreviewType as SwarmUserPost['linkPreviewType']) || undefined,
|
|
linkPreviewVideoUrl: row.linkPreviewVideoUrl || undefined,
|
|
linkPreviewMedia: parseLinkPreviewMediaJson(row.linkPreviewMediaJson),
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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: { 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,
|
|
};
|
|
|
|
const localPosts = await db.query.posts.findMany({
|
|
where: { AND: [{ userId: user.id }, { isRemoved: false }, { replyToId: { isNull: true } }, { swarmReplyToId: { isNull: true } }] },
|
|
with: profilePostRelations,
|
|
orderBy: (posts, { desc }) => [desc(posts.createdAt)],
|
|
limit: limit * 2,
|
|
});
|
|
|
|
const remoteRepostRows = await db.query.userSwarmReposts.findMany({
|
|
where: { userId: user.id },
|
|
orderBy: (userSwarmReposts, { desc }) => [desc(userSwarmReposts.repostedAt)],
|
|
limit: limit * 2,
|
|
});
|
|
|
|
const swarmPosts: SwarmUserPost[] = [
|
|
...localPosts.map((post) => mapLocalPostToSwarmPost(post, nodeDomain)),
|
|
...remoteRepostRows.map((row) => mapUserSwarmRepostToSwarmPost(row, user, nodeDomain)),
|
|
]
|
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
.slice(0, limit);
|
|
|
|
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 }
|
|
);
|
|
}
|
|
}
|