import React, { useState, useEffect, useRef } from 'react'; import { Rss, Filter, Plus, X, Copy, Check, ArrowRight, ShieldAlert, Settings, Radio, RefreshCw, Clock, AlertTriangle, Loader2 } from 'lucide-react'; const App = () => { const [step, setStep] = useState(1); // 1: Input, 2: Configure, 3: Result const [url, setUrl] = useState(''); const [channelName, setChannelName] = useState(''); const [includeKeywords, setIncludeKeywords] = useState([]); const [excludeKeywords, setExcludeKeywords] = useState([]); const [currentInput, setCurrentInput] = useState(''); const [filterType, setFilterType] = useState('include'); // 'include' or 'exclude' const [isCopied, setIsCopied] = useState(false); const [isLoading, setIsLoading] = useState(false); // Real Data State const [posts, setPosts] = useState([]); const [fetchError, setFetchError] = useState(null); const [isFetching, setIsFetching] = useState(false); // Update Interval State const [updateInterval, setUpdateInterval] = useState(25); const [lastUpdated, setLastUpdated] = useState(null); const refreshTimerRef = useRef(null); // Helper: Extract channel name const parseChannelName = (input) => { const match = input.match(/(?:t\.me\/|telegram\.me\/|@)([\w\d_]+)/); return match ? match[1] : ''; }; const handleUrlChange = (e) => { const val = e.target.value; setUrl(val); setChannelName(parseChannelName(val)); }; // --- CORE LOGIC: Fetch Real Telegram Data --- const fetchTelegramPosts = async (channel) => { if (!channel) return; setIsFetching(true); setFetchError(null); try { // Using a CORS proxy to bypass browser restrictions for the demo // In production backend, you would request https://t.me/s/${channel} directly const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`https://t.me/s/${channel}`)}`; const response = await fetch(proxyUrl); if (!response.ok) throw new Error('Network response was not ok'); const html = await response.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); // Parse Telegram Web Preview DOM const messageNodes = doc.querySelectorAll('.tgme_widget_message_wrap'); if (messageNodes.length === 0) { // Check if it's a valid channel page but empty or private if (html.includes('tgme_page_title')) { setPosts([]); // Valid channel, no posts found in preview } else { throw new Error('Канал не найден или является приватным'); } } else { const parsedPosts = Array.from(messageNodes).map(node => { const textNode = node.querySelector('.tgme_widget_message_text'); const rawText = textNode ? textNode.innerText : '[Медиа контент]'; // Handle photo-only posts const dateNode = node.querySelector('.tgme_widget_message_date time'); const dateStr = dateNode ? dateNode.getAttribute('datetime') : new Date().toISOString(); const linkNode = node.querySelector('.tgme_widget_message_date'); const postLink = linkNode ? linkNode.getAttribute('href') : '#'; // Extract first few words for title const title = rawText !== '[Медиа контент]' ? rawText.split(' ').slice(0, 6).join(' ') + (rawText.split(' ').length > 6 ? '...' : '') : 'Медиа пост'; return { id: postLink, // Use link as ID title: title, content: rawText, date: new Date(dateStr).toLocaleString('ru-RU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }), rawDate: new Date(dateStr) }; }); // Telegram shows oldest first in DOM usually, reverse to show newest first setPosts(parsedPosts.reverse()); setLastUpdated(new Date().toLocaleTimeString('ru-RU', {hour: '2-digit', minute:'2-digit'})); } } catch (err) { console.error(err); setFetchError("Не удалось загрузить ленту. Проверьте имя канала (он должен быть публичным)."); setPosts([]); } finally { setIsFetching(false); } }; // Step Transitions const goToConfigStep = () => { if (channelName) { setStep(2); fetchTelegramPosts(channelName); } }; // Auto-refresh logic for the preview useEffect(() => { if (step === 2 && updateInterval) { // Clear existing timer if (refreshTimerRef.current) clearInterval(refreshTimerRef.current); // Set new timer (converting minutes to ms) refreshTimerRef.current = setInterval(() => { fetchTelegramPosts(channelName); }, updateInterval * 60 * 1000); } return () => clearInterval(refreshTimerRef.current); }, [step, updateInterval, channelName]); const handleAddKeyword = () => { if (!currentInput.trim()) return; if (filterType === 'include') { if (!includeKeywords.includes(currentInput.trim())) { setIncludeKeywords([...includeKeywords, currentInput.trim()]); } } else { if (!excludeKeywords.includes(currentInput.trim())) { setExcludeKeywords([...excludeKeywords, currentInput.trim()]); } } setCurrentInput(''); }; const removeKeyword = (word, type) => { if (type === 'include') { setIncludeKeywords(includeKeywords.filter(k => k !== word)); } else { setExcludeKeywords(excludeKeywords.filter(k => k !== word)); } }; const handleGenerate = () => { setIsLoading(true); setTimeout(() => { setIsLoading(false); setStep(3); }, 1000); }; const getFilteredPosts = () => { if (!posts.length) return []; return posts.filter(post => { const text = (post.title + " " + post.content).toLowerCase(); // Logic: Must contain AT LEAST ONE included keyword (if any exist) const hasInclude = includeKeywords.length === 0 || includeKeywords.some(k => text.includes(k.toLowerCase())); // Logic: Must NOT contain ANY excluded keywords const hasExclude = excludeKeywords.some(k => text.includes(k.toLowerCase())); return hasInclude && !hasExclude; }); }; const generatedUrl = `http://digital-liven.com/rss/${channelName || 'channel'}?include=${includeKeywords.join(',')}&exclude=${excludeKeywords.join(',')}&interval=${updateInterval}m`; const copyToClipboard = () => { navigator.clipboard.writeText(generatedUrl); setIsCopied(true); setTimeout(() => setIsCopied(false), 2000); }; return (
{/* Header */}
setStep(1)}>
Digital Liven RSS
{/* Step 1: Input URL */} {step === 1 && (

RSS фиды для Telegram каналов

Создавайте RSS ленты из реальных публичных Telegram каналов. Фильтруйте контент по ключевым словам и получайте обновления.

e.key === 'Enter' && channelName && goToConfigStep()} />
{[ {icon: , title: "Реальные данные", desc: "Парсинг публичных каналов (t.me/s/...) в реальном времени."}, {icon: , title: "Умные фильтры", desc: "Включайте или исключайте посты по ключевым словам."}, {icon: , title: "Авто-обновление", desc: "Настройка интервала опроса источника."} ].map((item, idx) => (
{item.icon}

{item.title}

{item.desc}

))}
)} {/* Step 2: Configuration */} {step === 2 && (

Настройка фильтрации

Источник: @{channelName}

Live Config
{/* Left: Filter & Interval Controls */}
{/* Keywords Section */}
setCurrentInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleAddKeyword()} placeholder={filterType === 'include' ? "Например: news, важно..." : "Например: реклама, казино..."} className="flex-1 px-3 py-2 border border-slate-300 rounded-lg outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-100" />
{/* Chips Display */}
{includeKeywords.length > 0 && (
Обязательно содержит
{includeKeywords.map(k => ( {k} ))}
)} {excludeKeywords.length > 0 && (
Исключить, если содержит
{excludeKeywords.map(k => ( {k} ))}
)} {includeKeywords.length === 0 && excludeKeywords.length === 0 && (
Фильтры не заданы. Будут показаны все посты канала.
)}
{/* Update Interval Section */}
{updateInterval} мин
setUpdateInterval(e.target.value)} className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-blue-600" />
5 мин 30 мин 60 мин

Сервис будет открывать t.me/s/{channelName} каждые {updateInterval} мин, считывать новые посты и фильтровать их.

{/* Right: Live Preview */}

Предпросмотр (Live)

{lastUpdated && Обновлено: {lastUpdated}}
{/* Fetching State */} {isFetching && !posts.length && (
Загрузка данных канала...
)} {/* Error State */} {fetchError && (
{fetchError}
)} {/* Empty State / No Matches */} {!isFetching && !fetchError && getFilteredPosts().length === 0 && (

{posts.length === 0 ? "Постов не найдено" : "Нет постов, соответствующих фильтрам"}

)} {/* Content List */} {!isFetching && !fetchError && getFilteredPosts().length > 0 && (
{getFilteredPosts().map((post, idx) => (
{post.title} {post.date}

{post.content}

Открыть в Telegram
))}
)} {!isFetching && posts.length > 0 && (
Всего загружено: {posts.length} | Показано: {getFilteredPosts().length}
)}
)} {/* Step 3: Result */} {step === 3 && (

Ваша RSS лента готова!

Сервис настроен на мониторинг канала @{channelName}.

{generatedUrl}

Как это работает?

Ссылка содержит инструкции для бэкенда: периодически (раз в {updateInterval} мин) запрашивать t.me/s/{channelName}, парсить новые посты и отдавать XML только с теми, что прошли фильтры.

)}
); }; export default App;