Files
Synapsis/src/app/api/users/[handle]/replies/route.ts
T
cyph3rasi 21258edd95 Replace PostgreSQL and Docker deployment with embedded Turso, Drizzle relational queries v2, native systemd deployment on port 43821, and fresh SQLite migrations.
Hop-State: A_06FP5KEDBTB4A9ZT7QB498G
Hop-Proposal: R_06FP5KDWMCKVVMQZT4MVZ28
Hop-Task: T_06FP5DZ7T0G45FG93PT90B8
Hop-Attempt: AT_06FP5DZ7T0PKQW99V27JCV8
2026-07-14 15:44:42 -07:00

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 });
}
}