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;