feat(swarm): Implement decentralized node discovery and content federation system

- 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
This commit is contained in:
AskIt
2026-01-25 23:01:29 +01:00
parent 765845bd89
commit e2fa572e84
42 changed files with 10829 additions and 164 deletions
+129
View File
@@ -0,0 +1,129 @@
/**
* Swarm Timeline
*
* Fetches and aggregates posts from across the swarm network.
*/
import { getActiveSwarmNodes } from './registry';
import type { SwarmPost } from '@/app/api/swarm/timeline/route';
interface TimelineResult {
posts: SwarmPost[];
sources: { domain: string; postCount: number; isNsfw?: boolean; error?: string }[];
fetchedAt: string;
}
interface TimelineOptions {
includeNsfw?: boolean; // Whether to include NSFW content
}
/**
* Fetch timeline from a single node
*/
async function fetchNodeTimeline(
domain: string,
limit: number = 20
): Promise<{ posts: SwarmPost[]; nodeIsNsfw?: boolean; error?: string }> {
try {
const baseUrl = domain.startsWith('http') ? domain : `https://${domain}`;
const url = `${baseUrl}/api/swarm/timeline?limit=${limit}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000); // 5s timeout
const response = await fetch(url, {
headers: { 'Accept': 'application/json' },
signal: controller.signal,
});
clearTimeout(timeout);
if (!response.ok) {
return { posts: [], error: `HTTP ${response.status}` };
}
const data = await response.json();
return { posts: data.posts || [], nodeIsNsfw: data.nodeIsNsfw };
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
return { posts: [], error: message };
}
}
/**
* Fetch aggregated timeline from the swarm
*
* Queries multiple nodes in parallel and merges results.
* Filters out NSFW content unless explicitly requested.
*/
export async function fetchSwarmTimeline(
maxNodes: number = 10,
postsPerNode: number = 10,
options: TimelineOptions = {}
): Promise<TimelineResult> {
const { includeNsfw = false } = options;
// Get active nodes to query
const nodes = await getActiveSwarmNodes(maxNodes);
// Always include our own posts
const ourDomain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost';
// Filter out NSFW nodes if not including NSFW content
const eligibleNodes = includeNsfw
? nodes
: nodes.filter(n => !n.isNsfw);
const nodesToQuery = [
ourDomain,
...eligibleNodes.map(n => n.domain).filter(d => d !== ourDomain)
].slice(0, maxNodes);
// Fetch from all nodes in parallel
const results = await Promise.all(
nodesToQuery.map(async (domain) => {
const result = await fetchNodeTimeline(domain, postsPerNode);
return {
domain,
...result,
};
})
);
// Collect all posts and track sources
const allPosts: SwarmPost[] = [];
const sources: TimelineResult['sources'] = [];
for (const result of results) {
sources.push({
domain: result.domain,
postCount: result.posts.length,
isNsfw: result.nodeIsNsfw,
error: result.error,
});
// Filter NSFW posts if not including NSFW
const filteredPosts = includeNsfw
? result.posts
: result.posts.filter(p => !p.isNsfw);
allPosts.push(...filteredPosts);
}
// Sort by createdAt descending and dedupe by id
const seen = new Set<string>();
const uniquePosts = allPosts
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.filter(post => {
const key = `${post.nodeDomain}:${post.id}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
return {
posts: uniquePosts,
sources,
fetchedAt: new Date().toISOString(),
};
}