Fix reply consistency and source validation
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import type { ContentSourceConfig } from './contentSource';
|
||||
import { ContentSourceValidationError, validateContentSourceConfig } from './contentSource';
|
||||
import { fetchRSSFeed, fetchRedditPosts, fetchNewsApi, fetchBraveNews, NetworkError, ParseError } from './contentFetcher';
|
||||
import type { FeedItem } from './rssParser';
|
||||
|
||||
const VALIDATION_TIMEOUT_MS = 10000;
|
||||
|
||||
export async function validateSourceReachability(config: ContentSourceConfig): Promise<void> {
|
||||
const validation = validateContentSourceConfig(config);
|
||||
if (!validation.valid) {
|
||||
throw new ContentSourceValidationError(
|
||||
`Invalid content source configuration: ${validation.errors.join(', ')}`,
|
||||
validation.errors
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
let items: FeedItem[] = [];
|
||||
|
||||
switch (config.type) {
|
||||
case 'rss':
|
||||
case 'youtube':
|
||||
items = await fetchRSSFeed(config.url, { maxItems: 1, timeout: VALIDATION_TIMEOUT_MS });
|
||||
break;
|
||||
|
||||
case 'reddit':
|
||||
items = await fetchRedditPosts(
|
||||
config.subreddit || '',
|
||||
{ maxItems: 1, timeout: VALIDATION_TIMEOUT_MS }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'news_api':
|
||||
items = await fetchNewsApi(
|
||||
config.url,
|
||||
config.apiKey || '',
|
||||
{ maxItems: 1, timeout: VALIDATION_TIMEOUT_MS }
|
||||
);
|
||||
break;
|
||||
|
||||
case 'brave_news':
|
||||
items = await fetchBraveNews(
|
||||
config.braveNewsConfig || { query: '' },
|
||||
config.apiKey || '',
|
||||
{ maxItems: 1, timeout: VALIDATION_TIMEOUT_MS }
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
items = [];
|
||||
}
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
throw new ContentSourceValidationError(
|
||||
'Source is reachable but returned no items',
|
||||
['Source is reachable but returned no items']
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ContentSourceValidationError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof NetworkError || error instanceof ParseError) {
|
||||
throw new ContentSourceValidationError(
|
||||
`Could not validate source: ${error.message}`,
|
||||
[error.message]
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
throw new ContentSourceValidationError(
|
||||
`Could not validate source: ${error.message}`,
|
||||
[error.message]
|
||||
);
|
||||
}
|
||||
|
||||
throw new ContentSourceValidationError(
|
||||
'Could not validate source',
|
||||
['Could not validate source']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,16 @@ interface TimelineOptions {
|
||||
cursor?: string; // Timestamp cursor for pagination
|
||||
}
|
||||
|
||||
function isReplyPost(post: SwarmPost): boolean {
|
||||
return Boolean(
|
||||
post.isReply ||
|
||||
post.replyToId ||
|
||||
post.swarmReplyToId ||
|
||||
// Defensive against older or non-conforming node payloads.
|
||||
(post as SwarmPost & { replyTo?: unknown }).replyTo
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first URL from post content
|
||||
*/
|
||||
@@ -183,16 +193,19 @@ export async function fetchSwarmTimeline(
|
||||
const sources: TimelineResult['sources'] = [];
|
||||
|
||||
for (const result of results) {
|
||||
const nonReplyPosts = result.posts.filter(post => !isReplyPost(post));
|
||||
|
||||
// Filter NSFW posts only if user doesn't want NSFW content
|
||||
// A post is NSFW if it's explicitly marked OR comes from an NSFW node
|
||||
const filteredPosts = includeNsfw
|
||||
? result.posts
|
||||
: result.posts.filter(p => !p.isNsfw && !p.nodeIsNsfw);
|
||||
? nonReplyPosts
|
||||
: nonReplyPosts.filter(p => !p.isNsfw && !p.nodeIsNsfw);
|
||||
|
||||
// Log filtering details for debugging
|
||||
const nsfwPosts = result.posts.filter(p => p.isNsfw);
|
||||
const nodeNsfwPosts = result.posts.filter(p => p.nodeIsNsfw);
|
||||
console.log(`[Swarm Timeline] ${result.domain}: ${result.posts.length} posts fetched, ${nsfwPosts.length} marked NSFW, ${nodeNsfwPosts.length} from NSFW node, ${filteredPosts.length} after filter (includeNsfw: ${includeNsfw})`);
|
||||
const nsfwPosts = nonReplyPosts.filter(p => p.isNsfw);
|
||||
const nodeNsfwPosts = nonReplyPosts.filter(p => p.nodeIsNsfw);
|
||||
const replyPosts = result.posts.length - nonReplyPosts.length;
|
||||
console.log(`[Swarm Timeline] ${result.domain}: ${result.posts.length} posts fetched, ${replyPosts} replies filtered, ${nsfwPosts.length} marked NSFW, ${nodeNsfwPosts.length} from NSFW node, ${filteredPosts.length} after filter (includeNsfw: ${includeNsfw})`);
|
||||
|
||||
sources.push({
|
||||
domain: result.domain,
|
||||
|
||||
@@ -27,6 +27,15 @@ export function formatFullHandle(handle: string, nodeDomain?: string | null): st
|
||||
return `@${cleanHandle}@${domain}`;
|
||||
}
|
||||
|
||||
export function getProfilePath(handle: string): string {
|
||||
if (!handle) {
|
||||
return '/u';
|
||||
}
|
||||
|
||||
const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle;
|
||||
return `/u/${cleanHandle}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that formats a handle using the runtime domain config.
|
||||
* Use this in client components instead of formatFullHandle for local handles.
|
||||
|
||||
Reference in New Issue
Block a user