e2fa572e84
- Add swarm node registry with discovery, gossip, and timeline synchronization - Implement database schema for swarm nodes, seeds, and sync logs with proper indexing - Create swarm API endpoints for node discovery, gossip protocol, timeline sharing, and announcements - Add content moderation features with NSFW flagging and muted nodes support - Implement user settings pages for content filtering and node moderation - Add background scheduler for automated swarm synchronization and node health checks - Create .well-known endpoint for swarm protocol discovery - Remove legacy bot-cron.ts in favor of integrated scheduler system - Add user account NSFW preferences and age verification tracking - Update database schema with swarm-related tables and user moderation fields - Enhance post and user components to support federated content display - Add instrumentation for monitoring swarm operations and node health
123 lines
3.4 KiB
TypeScript
123 lines
3.4 KiB
TypeScript
/**
|
|
* Swarm Timeline Endpoint
|
|
*
|
|
* GET: Returns recent public posts from this node for the swarm timeline
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { db, posts, users, media, nodes } from '@/db';
|
|
import { eq, desc, and, isNull } from 'drizzle-orm';
|
|
|
|
export interface SwarmPost {
|
|
id: string;
|
|
content: string;
|
|
createdAt: string;
|
|
author: {
|
|
handle: string;
|
|
displayName: string;
|
|
avatarUrl?: string;
|
|
isNsfw: boolean;
|
|
};
|
|
nodeDomain: string;
|
|
nodeIsNsfw: boolean;
|
|
isNsfw: boolean;
|
|
likeCount: number;
|
|
repostCount: number;
|
|
replyCount: number;
|
|
mediaUrls?: string[];
|
|
}
|
|
|
|
/**
|
|
* GET /api/swarm/timeline
|
|
*
|
|
* Returns recent public posts from this node.
|
|
* Used by other nodes to build the swarm-wide timeline.
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);
|
|
|
|
if (!db) {
|
|
return NextResponse.json({ posts: [], nodeDomain: '', nodeIsNsfw: false });
|
|
}
|
|
|
|
const nodeDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
|
|
|
|
// Get node NSFW status
|
|
const node = await db.query.nodes.findFirst({
|
|
where: eq(nodes.domain, nodeDomain),
|
|
});
|
|
const nodeIsNsfw = node?.isNsfw ?? false;
|
|
|
|
// Get recent public posts (not replies, local users only, not removed)
|
|
const recentPosts = await db
|
|
.select({
|
|
id: posts.id,
|
|
content: posts.content,
|
|
createdAt: posts.createdAt,
|
|
isNsfw: posts.isNsfw,
|
|
likesCount: posts.likesCount,
|
|
repostsCount: posts.repostsCount,
|
|
repliesCount: posts.repliesCount,
|
|
authorHandle: users.handle,
|
|
authorDisplayName: users.displayName,
|
|
authorAvatarUrl: users.avatarUrl,
|
|
authorIsNsfw: users.isNsfw,
|
|
authorNodeId: users.nodeId,
|
|
})
|
|
.from(posts)
|
|
.innerJoin(users, eq(posts.userId, users.id))
|
|
.where(
|
|
and(
|
|
isNull(posts.replyToId), // Not a reply
|
|
eq(posts.isRemoved, false) // Not removed
|
|
)
|
|
)
|
|
.orderBy(desc(posts.createdAt))
|
|
.limit(limit);
|
|
|
|
// Fetch media for each post
|
|
const swarmPosts: SwarmPost[] = [];
|
|
|
|
for (const post of recentPosts) {
|
|
const postMedia = await db
|
|
.select({ url: media.url })
|
|
.from(media)
|
|
.where(eq(media.postId, post.id));
|
|
|
|
swarmPosts.push({
|
|
id: post.id,
|
|
content: post.content,
|
|
createdAt: post.createdAt.toISOString(),
|
|
author: {
|
|
handle: post.authorHandle,
|
|
displayName: post.authorDisplayName || post.authorHandle,
|
|
avatarUrl: post.authorAvatarUrl || undefined,
|
|
isNsfw: post.authorIsNsfw,
|
|
},
|
|
nodeDomain,
|
|
nodeIsNsfw,
|
|
isNsfw: post.isNsfw || post.authorIsNsfw || nodeIsNsfw, // Cascade NSFW flag
|
|
likeCount: post.likesCount,
|
|
repostCount: post.repostsCount,
|
|
replyCount: post.repliesCount,
|
|
mediaUrls: postMedia.length > 0 ? postMedia.map(m => m.url) : undefined,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
posts: swarmPosts,
|
|
nodeDomain,
|
|
nodeIsNsfw,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
} catch (error) {
|
|
console.error('Swarm timeline error:', error);
|
|
return NextResponse.json(
|
|
{ error: 'Failed to fetch timeline' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|