Files
rainblogweb/routes/feed.js
T

165 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const db = require('../db');
const router = express.Router();
// XML 转义:& < > " '
function escapeXml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
// 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;