21258edd95
Hop-State: A_06FP5KEDBTB4A9ZT7QB498G Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28 Hop-Task: T_06FP5DZ7T0G45FG93PT90B8 Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { db, likes, posts, users } from '@/db';
|
|
import { and, desc, eq, inArray, lt, not, or, isNotNull } from 'drizzle-orm';
|
|
import { discoverNode } from '@/lib/swarm/discovery';
|
|
import { getRemoteBaseUrl, mapRemoteProfilePost, parseRemoteHandle } from '@/lib/swarm/remote-profile-posts';
|
|
import { isSwarmNode } from '@/lib/swarm/interactions';
|
|
import { getViewerSwarmRepostedPostIds } from '@/lib/swarm/reposts';
|
|
|
|
const embeddedPostRelations = {
|
|
author: true,
|
|
bot: true,
|
|
media: true,
|
|
replyTo: {
|
|
with: {
|
|
author: true,
|
|
bot: true,
|
|
media: true,
|
|
},
|
|
},
|
|
} as const;
|
|
|
|
const replyRelations = {
|
|
...embeddedPostRelations,
|
|
repostOf: {
|
|
with: embeddedPostRelations,
|
|
},
|
|
} as const;
|
|
|
|
type RouteContext = { params: Promise<{ handle: string }> };
|
|
|
|
export async function GET(request: Request, 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);
|
|
const cursor = searchParams.get('cursor');
|
|
const remote = parseRemoteHandle(handle);
|
|
|
|
const fetchRemoteReplies = async () => {
|
|
if (!remote) {
|
|
return NextResponse.json({ posts: [], nextCursor: null });
|
|
}
|
|
|
|
const baseUrl = getRemoteBaseUrl(remote.domain);
|
|
const res = await fetch(`${baseUrl}/api/users/${remote.handle}/replies?limit=${limit}`, {
|
|
headers: { Accept: 'application/json' },
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
return NextResponse.json({ posts: [], nextCursor: null });
|
|
}
|
|
|
|
const data = await res.json();
|
|
const { getSession } = await import('@/lib/auth');
|
|
const session = await getSession();
|
|
const viewer = session?.user;
|
|
const mappedPosts = (data.posts || []).map((post: any) => mapRemoteProfilePost(post, remote.domain));
|
|
const repostedIds = viewer
|
|
? await getViewerSwarmRepostedPostIds(
|
|
mappedPosts.map((post: any) => ({
|
|
id: post.id,
|
|
nodeDomain: remote.domain,
|
|
originalPostId: post.originalPostId || post.id.split(':').pop(),
|
|
})),
|
|
viewer.id
|
|
)
|
|
: new Set<string>();
|
|
return NextResponse.json({
|
|
posts: mappedPosts.map((post: any) => ({
|
|
...post,
|
|
isReposted: repostedIds.has(post.id),
|
|
})),
|
|
nextCursor: null,
|
|
});
|
|
};
|
|
|
|
if (!db) {
|
|
if (!remote) {
|
|
return NextResponse.json({ posts: [], nextCursor: null });
|
|
}
|
|
|
|
let swarm = await isSwarmNode(remote.domain);
|
|
if (!swarm) {
|
|
const discovery = await discoverNode(remote.domain);
|
|
swarm = discovery.success;
|
|
}
|
|
|
|
if (!swarm) {
|
|
return NextResponse.json({ posts: [], nextCursor: null });
|
|
}
|
|
|
|
return await fetchRemoteReplies();
|
|
}
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: { handle: cleanHandle },
|
|
});
|
|
const isRemotePlaceholder = user && cleanHandle.includes('@');
|
|
|
|
if (!user || isRemotePlaceholder) {
|
|
if (!remote) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
let swarm = await isSwarmNode(remote.domain);
|
|
if (!swarm) {
|
|
const discovery = await discoverNode(remote.domain);
|
|
swarm = discovery.success;
|
|
}
|
|
|
|
if (!swarm) {
|
|
return NextResponse.json({ posts: [], nextCursor: null });
|
|
}
|
|
|
|
return await fetchRemoteReplies();
|
|
}
|
|
|
|
if (user.isSuspended) {
|
|
return NextResponse.json({ error: 'User not found' }, { status: 404 });
|
|
}
|
|
|
|
let cursorDate: Date | undefined;
|
|
|
|
if (cursor) {
|
|
const cursorPost = await db.query.posts.findFirst({
|
|
where: { id: cursor },
|
|
});
|
|
if (cursorPost) {
|
|
cursorDate = cursorPost.createdAt;
|
|
}
|
|
}
|
|
|
|
let replyPosts: any[] = await db.query.posts.findMany({
|
|
where: {
|
|
userId: user.id,
|
|
isRemoved: false,
|
|
OR: [
|
|
{ replyToId: { isNotNull: true } },
|
|
{ swarmReplyToId: { isNotNull: true } },
|
|
],
|
|
...(cursorDate ? { createdAt: { lt: cursorDate } } : {}),
|
|
},
|
|
with: replyRelations,
|
|
orderBy: () => [desc(posts.createdAt)],
|
|
limit,
|
|
});
|
|
|
|
try {
|
|
const { getSession } = await import('@/lib/auth');
|
|
const session = await getSession();
|
|
|
|
if (session?.user && replyPosts.length > 0) {
|
|
const viewer = session.user;
|
|
const postIds = replyPosts.map((post) => post.id).filter(Boolean);
|
|
|
|
const viewerLikes = postIds.length > 0
|
|
? await db.query.likes.findMany({
|
|
where: { AND: [{ userId: viewer.id }, { postId: { in: postIds } }] },
|
|
})
|
|
: [];
|
|
const likedPostIds = new Set(viewerLikes.map((like) => like.postId));
|
|
|
|
const viewerReposts = postIds.length > 0
|
|
? await db.query.posts.findMany({
|
|
where: { AND: [{ userId: viewer.id }, { repostOfId: { in: postIds } }, { isRemoved: false }] },
|
|
})
|
|
: [];
|
|
const repostedPostIds = new Set(viewerReposts.map((post) => post.repostOfId));
|
|
|
|
replyPosts = replyPosts.map((post) => ({
|
|
...post,
|
|
isLiked: likedPostIds.has(post.id),
|
|
isReposted: repostedPostIds.has(post.id),
|
|
}));
|
|
}
|
|
} catch {
|
|
}
|
|
|
|
return NextResponse.json({
|
|
posts: replyPosts,
|
|
nextCursor: replyPosts.length === limit ? replyPosts[replyPosts.length - 1]?.id : null,
|
|
});
|
|
} catch (error) {
|
|
console.error('Get user replies error:', error);
|
|
return NextResponse.json({ error: 'Failed to get replies' }, { status: 500 });
|
|
}
|
|
}
|