import { parseFeed } from 'feedsmith'; import feeds from './feeds.json' with { type: "json" }; export default { async scheduled(event, env, ctx) { ctx.waitUntil(checkAllFeeds(env)); }, async fetch(request, env) { const path = new URL(request.url).pathname; if (path === '/check') { await checkAllFeeds(env); return new Response('Checked all feeds'); } return new Response('Multi-feed monitor. GET /check to trigger manually.'); } }; async function checkAllFeeds(env) { const webhooks = JSON.parse(env.WEBHOOKS); for (const [feedUrl, channels] of Object.entries(feeds)) { try { await checkFeed(env, feedUrl, channels, webhooks); } catch (e) { console.error(`Error checking ${feedUrl}:`, e.message); } } } async function checkFeed(env, feedUrl, channels, webhooks) { const res = await fetch(feedUrl); if (!res.ok) return console.error(`Feed fetch failed: ${res.status}`); const text = await res.text(); const entries = parseEntries(text); if (!entries.length) return; const kvKey = `last:${new URL(feedUrl).pathname}`; const lastPosted = await env.KV.get(kvKey); const lastDate = lastPosted ? new Date(lastPosted) : new Date(0); const newEntries = entries .filter(e => e.date && new Date(e.date) > lastDate) .sort((a, b) => new Date(a.date) - new Date(b.date)); console.log(`Feed ${feedUrl}: ${entries.length} entries, ${newEntries.length} new (last: ${lastPosted || 'none'})`); if (!newEntries.length) return; for (const entry of newEntries) { for (const channel of channels) { const webhook = webhooks[channel]; if (webhook) await postToDiscord(webhook, entry, feedUrl); } } const latestDate = newEntries[newEntries.length - 1].date; await env.KV.put(kvKey, latestDate); } function parseEntries(text) { const { format, feed } = parseFeed(text); if (format === 'atom') { return (feed.entries || []).map(e => ({ title: e.title, link: e.links?.find(l => !l.rel || l.rel === 'alternate')?.href || e.links?.[0]?.href, summary: e.summary || e.content, date: e.updated || e.published, author: e.authors?.[0]?.name, })); } // RSS, RDF, JSON return (feed.items || []).map(e => ({ title: e.title, link: e.link, summary: e.description, date: e.pubDate, author: e.authors?.[0], })); } function stripHtml(html) { return html .replace(//gi, '\n') .replace(/<\/li>/gi, '\n') .replace(/<\/p>/gi, '\n') .replace(/<[^>]+>/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); } async function postToDiscord(webhook, entry, feedUrl) { const feedName = new URL(feedUrl).pathname.split('/')[1] || 'Feed'; const res = await fetch(webhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ embeds: [{ title: entry.title || 'New activity', url: entry.link || feedUrl, description: entry.summary ? stripHtml(entry.summary).slice(0, 300) : '', color: 0x5865F2, timestamp: entry.date ? new Date(entry.date).toISOString() : new Date().toISOString(), footer: { text: entry.author ? `${feedName} • ${entry.author}` : feedName } }] }) }); const body = await res.text(); console.log(`Discord ${res.status}: ${body}`); }