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
This commit is contained in:
Christomatt
2026-01-26 10:43:08 +01:00
parent fce820d59a
commit 5efd3adbfd
4 changed files with 288 additions and 0 deletions
@@ -4,6 +4,24 @@ import { eq } from 'drizzle-orm';
type RouteContext = { params: Promise<{ handle: string }> };
/**
* Fetch followers list from a remote swarm node
*/
const fetchSwarmFollowers = async (handle: string, domain: string, limit: number) => {
try {
const protocol = domain.includes('localhost') ? 'http' : 'https';
const url = `${protocol}://${domain}/api/swarm/users/${handle}/followers?limit=${limit}`;
const res = await fetch(url, {
headers: { 'Accept': 'application/json' },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
};
export async function GET(request: Request, context: RouteContext) {
try {
const { handle } = await context.params;
@@ -11,6 +29,29 @@ export async function GET(request: Request, context: RouteContext) {
const { searchParams } = new URL(request.url);
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 50);
// Check if this is a remote user
const [remoteHandle, remoteDomain] = cleanHandle.split('@');
if (remoteDomain) {
// Fetch from remote swarm node
const swarmData = await fetchSwarmFollowers(remoteHandle, remoteDomain, limit);
if (swarmData?.followers) {
// Transform to include full handles for local users on that node
const followers = swarmData.followers.map((f: any) => ({
id: f.isRemote ? f.handle : `${f.handle}@${remoteDomain}`,
handle: f.isRemote ? f.handle : `${f.handle}@${remoteDomain}`,
displayName: f.displayName,
avatarUrl: f.avatarUrl,
bio: f.bio,
isRemote: true,
isBot: f.isBot,
}));
return NextResponse.json({ followers, nextCursor: null });
}
// If swarm fetch fails, return empty
return NextResponse.json({ followers: [], nextCursor: null });
}
// Return empty if no database
if (!db) {
return NextResponse.json({ followers: [], nextCursor: null });