165 lines
6.2 KiB
JavaScript
165 lines
6.2 KiB
JavaScript
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, '"')
|
||
.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, ']]]]><![CDATA[>');
|
||
const authorXml = it.author
|
||
? ` <dc:creator><![CDATA[${String(it.author).replace(/\]\]>/g, ']]]]><![CDATA[>')}]]></dc:creator>\n`
|
||
: '';
|
||
itemsXml += ` <item>
|
||
<title>${escapeXml(it.title)}</title>
|
||
<link>${escapeXml(it.link)}</link>
|
||
<guid>${escapeXml(it.link)}</guid>
|
||
<pubDate>${escapeXml(toRfc822(it.pubDate))}</pubDate>
|
||
${authorXml} <description><![CDATA[${descCdata}]]></description>
|
||
</item>
|
||
`;
|
||
});
|
||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||
<channel>
|
||
<title>${escapeXml(title)}</title>
|
||
<link>${escapeXml(link)}</link>
|
||
<description>${escapeXml(description)}</description>
|
||
${itemsXml} </channel>
|
||
</rss>
|
||
`;
|
||
}
|
||
|
||
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;
|