const express = require('express'); const db = require('../db'); const router = express.Router(); // XML 转义:& < > " ' function escapeXml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } // created_at('YYYY-MM-DD HH:MM:SS',UTC)转 RFC822 格式('ddd, DD MMM YYYY HH:MM:SS GMT') function toRfc822(createdAt) { try { const d = new Date(String(createdAt).replace(' ', 'T') + 'Z'); return isNaN(d.getTime()) ? String(createdAt) : d.toUTCString(); } catch { return String(createdAt); } } // 摘要:剥 markdown/自定义标签符号,截断 ~maxLen 字 // 处理 [image:]/[file:] 标签、代码块/行内代码、链接(留文字)、标题#、强调*/_/~、引用>、列表符号 function makeSummary(content, maxLen = 200) { let s = String(content || '') .replace(/\[image:[^\]]*\]/g, ' ') .replace(/\[file:[^\]]*\]/g, ' ') .replace(/```[\s\S]*?```/g, ' ') .replace(/`([^`]*)`/g, '$1') .replace(/!\[[^\]]*\]\([^)]*\)/g, ' ') .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') .replace(/^#{1,6}\s*/gm, '') .replace(/[*_~]{1,3}/g, '') .replace(/^\s*>\s?/gm, '') .replace(/^\s*[-+*]\s+/gm, '') .replace(/\s+/g, ' ') .trim(); if (s.length > maxLen) s = s.slice(0, maxLen) + '…'; return s; } // RSS 2.0 生成器(博客/论坛全站/版块三源共用) // items = [{ title, link, description, author?, pubDate }] function renderRss({ title, description, link, items }) { let itemsXml = ''; items.forEach(it => { const descCdata = String(it.description || '').replace(/\]\]>/g, ']]]]>'); const authorXml = it.author ? ` /g, ']]]]>')}]]>\n` : ''; itemsXml += ` ${escapeXml(it.title)} ${escapeXml(it.link)} ${escapeXml(it.link)} ${escapeXml(toRfc822(it.pubDate))} ${authorXml} `; }); return ` ${escapeXml(title)} ${escapeXml(link)} ${escapeXml(description)} ${itemsXml} `; } function sendFeed(res, xml) { // feed 要新鲜:禁缓存(RSS 阅读器拉取频繁,旧缓存会推迟新帖出现) res.header('Content-Type', 'application/rss+xml; charset=utf-8'); res.header('Cache-Control', 'no-cache'); res.send(xml); } // 论坛源对外条件:全站开关 feed_forum_enabled='1' 且论坛游客可见(私密模式不对外订阅) function forumFeedEnabled() { return db.getSetting('feed_forum_enabled') === '1' && db.getSetting('forum_guest_visible') === '1'; } // 论坛帖子 → feed item(摘要剥离 markdown) function forumItem(p, base) { return { title: p.title, link: base + '/forum/' + p.id, description: makeSummary(p.content), author: p.username || '匿名', pubDate: p.created_at }; } function siteBase(req) { const siteUrl = db.getSetting('site_url') || (req.protocol + '://' + req.get('host')); return siteUrl.replace(/\/$/, ''); } function feedMaxItems() { return Math.min(Math.max(parseInt(db.getSetting('feed_max_items')) || 20, 1), 100); } // ── 博客全站源(原有 /feed.xml 保留,内容受 feed_show_full / feed_max_items 控制)──────── router.get('/feed.xml', (req, res) => { const siteName = db.getSetting('site_name') || 'RainWeb'; const siteDesc = db.getSetting('site_description') || '个人云平台'; const base = siteBase(req); const maxItems = feedMaxItems(); const showFull = db.getSetting('feed_show_full') === '1'; const posts = db.all( 'SELECT id, title, excerpt, content, created_at FROM blog_posts WHERE published = 1 ORDER BY created_at DESC LIMIT ?', [maxItems]); const items = posts.map(p => ({ title: p.title, link: base + '/blog/' + p.id, // 全文模式用 content;摘要模式优先 excerpt,无 excerpt 则剥 markdown 取前 200 字 description: showFull ? (p.content || '') : (p.excerpt || makeSummary(p.content)), pubDate: p.created_at })); sendFeed(res, renderRss({ title: siteName, description: siteDesc, link: base, items })); }); // ── 论坛全站源 ────────────────────────────────── router.get('/feed/forum.xml', (req, res) => { if (!forumFeedEnabled()) return res.status(404).send('Not found'); const siteName = db.getSetting('site_name') || 'RainWeb'; const siteDesc = db.getSetting('site_description') || '个人云平台'; const base = siteBase(req); const maxItems = feedMaxItems(); const posts = db.all( `SELECT fp.*, u.username FROM forum_posts fp JOIN users u ON fp.author_id = u.id ORDER BY fp.created_at DESC LIMIT ?`, [maxItems]); sendFeed(res, renderRss({ title: siteName + ' 论坛', description: siteDesc + ' —— 论坛最新帖子', link: base + '/forum.html', items: posts.map(p => forumItem(p, base)) })); }); // ── 版块源 ───────────────────────────────────── router.get('/feed/forum/c/:id.xml', (req, res) => { if (!forumFeedEnabled()) return res.status(404).send('Not found'); const cat = db.get('SELECT id, name, feed_enabled FROM forum_categories WHERE id = ?', [req.params.id]); // 版块不存在或该版块 feed 已关闭 → 404 if (!cat || !cat.feed_enabled) return res.status(404).send('Not found'); const siteName = db.getSetting('site_name') || 'RainWeb'; const base = siteBase(req); const maxItems = feedMaxItems(); const posts = db.all( `SELECT fp.*, u.username FROM forum_posts fp JOIN users u ON fp.author_id = u.id WHERE fp.category_id = ? ORDER BY fp.created_at DESC LIMIT ?`, [cat.id, maxItems]); sendFeed(res, renderRss({ title: cat.name + ' - ' + siteName, description: cat.name + ' 版块最新帖子', link: base + '/forum/c/' + cat.id, items: posts.map(p => forumItem(p, base)) })); }); module.exports = router;