Files
Synapsis/src/app/api/swarm/users/[handle]/followers/route.ts
T
Christomatt 5efd3adbfd feat(swarm): Add federated followers and following endpoints
- Create new swarm endpoints for fetching user followers list at /api/swarm/users/[handle]/followers
- Create new swarm endpoints for fetching user following list at /api/swarm/users/[handle]/following
- Add SwarmFollowerUser and SwarmFollowingUser interfaces for federated user data
- Implement local and remote follower/following aggregation in swarm endpoints
- Update local followers endpoint to fetch data from remote swarm nodes
- Update local following endpoint to fetch data from remote swarm nodes
- Add error handling and database availability checks for all endpoints
- Include node domain and timestamp metadata in swarm responses
- Support pagination with configurable limit parameter (max 100)
- Enable cross-node follower/following visibility for federated social graph
2026-01-26 10:43:08 +01:00

104 lines
3.0 KiB
TypeScript

/**
* Swarm User Followers Endpoint
*
* GET: Returns a user's followers list for swarm requests
*/
import { NextRequest, NextResponse } from 'next/server';
import { db, follows, users, remoteFollowers } from '@/db';
import { eq } from 'drizzle-orm';
export interface SwarmFollowerUser {
handle: string;
displayName: string;
avatarUrl?: string;
bio?: string;
isBot?: boolean;
isRemote?: boolean;
}
type RouteContext = { params: Promise<{ handle: string }> };
/**
* GET /api/swarm/users/[handle]/followers
*
* Returns a user's followers list.
* Used by other nodes to display who follows a remote user.
*/
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') || '50'), 100);
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),
});
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
if (user.isSuspended) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
// Get local followers
const userFollowers = await db
.select({
id: follows.id,
follower: users,
})
.from(follows)
.innerJoin(users, eq(follows.followerId, users.id))
.where(eq(follows.followingId, user.id))
.limit(limit);
const localFollowers: SwarmFollowerUser[] = userFollowers.map(f => ({
handle: f.follower.handle, // Local handle without domain
displayName: f.follower.displayName || f.follower.handle,
avatarUrl: f.follower.avatarUrl || undefined,
bio: f.follower.bio || undefined,
isBot: f.follower.isBot || undefined,
isRemote: false,
}));
// Get remote followers
const userRemoteFollowers = await db.query.remoteFollowers.findMany({
where: eq(remoteFollowers.userId, user.id),
limit,
});
const remoteFollowersList: SwarmFollowerUser[] = userRemoteFollowers.map(f => ({
handle: f.handle || 'unknown', // Remote handle with @domain
displayName: f.handle?.split('@')[0] || 'Unknown',
avatarUrl: undefined,
bio: undefined,
isRemote: true,
}));
// Merge all followers
const allFollowers = [...localFollowers, ...remoteFollowersList].slice(0, limit);
return NextResponse.json({
followers: allFollowers,
nodeDomain,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Swarm user followers error:', error);
return NextResponse.json(
{ error: 'Failed to fetch followers list' },
{ status: 500 }
);
}
}