import { v4 as uuidv4 } from 'uuid'; import { RSSFeed, RSSArticle } from '../types'; import { saveRSSFeed, updateRSSFeedLastFetched } from './storageService'; // CORS proxy for RSS feeds const RSS_PROXY = (url: string) => `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`; /** * Parse RSS/Atom feed XML into articles */ const parseRSSFeed = (xml: string, feedId: string): RSSArticle[] => { const parser = new DOMParser(); const doc = parser.parseFromString(xml, 'text/xml'); const articles: RSSArticle[] = []; // Try RSS 2.0 format const items = doc.querySelectorAll('item'); if (items.length > 0) { items.forEach(item => { const title = item.querySelector('title')?.textContent || ''; const link = item.querySelector('link')?.textContent || ''; const description = item.querySelector('description')?.textContent || ''; const pubDate = item.querySelector('pubDate')?.textContent || ''; if (title && link) { articles.push({ title: title.trim(), url: link.trim(), description: description.replace(/<[^>]*>/g, '').substring(0, 200), pubDate, feedId }); } }); return articles; } // Try Atom format const entries = doc.querySelectorAll('entry'); entries.forEach(entry => { const title = entry.querySelector('title')?.textContent || ''; const link = entry.querySelector('link')?.getAttribute('href') || ''; const summary = entry.querySelector('summary')?.textContent || ''; const published = entry.querySelector('published')?.textContent || ''; if (title && link) { articles.push({ title: title.trim(), url: link.trim(), description: summary.replace(/<[^>]*>/g, '').substring(0, 200), pubDate: published, feedId }); } }); return articles; }; /** * Get feed title from XML */ const getFeedTitle = (xml: string): string => { const parser = new DOMParser(); const doc = parser.parseFromString(xml, 'text/xml'); // RSS 2.0 const channelTitle = doc.querySelector('channel > title')?.textContent; if (channelTitle) return channelTitle.trim(); // Atom const feedTitle = doc.querySelector('feed > title')?.textContent; if (feedTitle) return feedTitle.trim(); return 'Unknown Feed'; }; /** * Fetch and parse RSS feed */ export const fetchRSSFeed = async (feedUrl: string): Promise<{ feed: RSSFeed; articles: RSSArticle[]; }> => { const response = await fetch(RSS_PROXY(feedUrl)); if (!response.ok) { throw new Error(`Failed to fetch feed: ${response.status}`); } const xml = await response.text(); const feedId = uuidv4(); const title = getFeedTitle(xml); const articles = parseRSSFeed(xml, feedId); const feed: RSSFeed = { id: feedId, url: feedUrl, title, articleCount: articles.length, lastFetched: Date.now(), isActive: true, addedAt: Date.now() }; return { feed, articles }; }; /** * Refresh articles from an existing feed */ export const refreshFeed = async (feed: RSSFeed): Promise => { const response = await fetch(RSS_PROXY(feed.url)); if (!response.ok) { throw new Error(`Failed to refresh feed: ${response.status}`); } const xml = await response.text(); const articles = parseRSSFeed(xml, feed.id); updateRSSFeedLastFetched(feed.id); return articles; }; /** * Validate RSS feed URL */ export const validateRSSUrl = async (url: string): Promise => { try { const response = await fetch(RSS_PROXY(url)); if (!response.ok) return false; const text = await response.text(); // Check if it looks like RSS/Atom return text.includes('