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:
@@ -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 });
|
||||
|
||||
@@ -4,6 +4,24 @@ import { eq } from 'drizzle-orm';
|
||||
|
||||
type RouteContext = { params: Promise<{ handle: string }> };
|
||||
|
||||
/**
|
||||
* Fetch following list from a remote swarm node
|
||||
*/
|
||||
const fetchSwarmFollowing = async (handle: string, domain: string, limit: number) => {
|
||||
try {
|
||||
const protocol = domain.includes('localhost') ? 'http' : 'https';
|
||||
const url = `${protocol}://${domain}/api/swarm/users/${handle}/following?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 fetchSwarmFollowing(remoteHandle, remoteDomain, limit);
|
||||
if (swarmData?.following) {
|
||||
// Transform to include full handles for local users on that node
|
||||
const following = swarmData.following.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({ following, nextCursor: null });
|
||||
}
|
||||
// If swarm fetch fails, return empty (could add ActivityPub fallback later)
|
||||
return NextResponse.json({ following: [], nextCursor: null });
|
||||
}
|
||||
|
||||
// Return empty if no database
|
||||
if (!db) {
|
||||
return NextResponse.json({ following: [], nextCursor: null });
|
||||
|
||||
Reference in New Issue
Block a user