码桶
发现社区成员的开源项目
index.js3.8 KB
/**
* 内置插件:RSS 订阅推送
* --------------------------------------------------
* 在「插件市场 → RSS 订阅 → 配置」里添加 RSS/Atom 订阅源后,
* 用户向机器人发送「最新」即可自动推送最新 3 条文章(标题 + 链接)。
*
* 零依赖:使用 axios 抓取、正则解析 RSS/Atom,无需额外安装 XML 库。
*/
const db = require('../../lib/db');
const axios = require('axios');
module.exports = {
meta: {
id: 'rss',
name: 'RSS 订阅推送',
version: '1.0.0',
author: '奶狗',
category: '信息获取',
description: '可添加多个 RSS/Atom 订阅源(名称 + 地址),发送「最新」即推送各订阅源最新 3 条文章。',
entry: 'rss/index.js',
builtin: true,
// 专用配置 UI:由前端渲染「名称 + 订阅地址」的多个订阅源管理,而非通用 key-value 表单
customConfig: 'rss_feeds',
},
async onMessage(msg, ctx) {
const text = (msg.content || '').trim();
// 触发指令:精确发送「最新」
if (text !== '最新') return false;
const botId = ctx.bot.id;
const feeds = await db.rows(
'SELECT id, title, feed_url FROM plugin_rss WHERE bot_id=? ORDER BY id DESC',
[botId]
);
if (!feeds.length) {
await ctx.sendText('[RSS] 尚未配置 RSS 订阅源,请在「插件市场 → RSS 订阅 → 配置」中添加。');
return true;
}
let sent = 0;
const MAX_TOTAL = 5;
for (const f of feeds) {
if (sent >= MAX_TOTAL) break;
try {
const xml = await fetchFeed(f.feed_url);
const items = parseFeed(xml).slice(0, 3);
for (const it of items) {
if (sent >= MAX_TOTAL) break;
const prefix = f.title ? '【' + f.title + '】\n' : '';
const body = '📰 ' + it.title + (it.link ? '\n' + it.link : '');
await ctx.sendText(prefix + body);
sent++;
}
} catch (e) {
console.error('[rss] 拉取失败', f.feed_url, e.message);
}
}
if (sent === 0) {
await ctx.sendText('[!] 暂时未能拉取到 RSS 内容,请检查订阅源地址是否有效。');
}
return true;
},
};
/** 抓取订阅源文本 */
async function fetchFeed(url) {
const { data } = await axios.get(url, {
timeout: 8000,
responseType: 'text',
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; RSSBot/1.0)' },
});
return typeof data === 'string' ? data : data.toString();
}
/** 解析 RSS 或 Atom,返回 [{title, link}] */
function parseFeed(xml) {
const out = [];
const collect = (blockRe, titleRe, linkRe) => {
let m;
while ((m = blockRe.exec(xml))) {
const block = m[1];
const t = (block.match(titleRe) || [])[1] || '';
let link = (block.match(linkRe) || [])[1] || '';
if (!link) {
// Atom: <link href="..."/>
link = (block.match(/<link[^>]+href="([^"]+)"/) || [])[1] || '';
}
if (t) out.push({ title: clean(t), link: clean(link) });
}
};
// RSS 2.0: <item><title>..</title><link>..</link>
collect(
/<item[\s>]([\s\S]*?)<\/item>/g,
/<title[^>]*>([\s\S]*?)<\/title>/,
/<link[^>]*>([\s\S]*?)<\/link>/
);
// Atom: <entry><title>..</title><link href=".."/>
if (!out.length) {
collect(
/<entry[\s>]([\s\S]*?)<\/entry>/g,
/<title[^>]*>([\s\S]*?)<\/title>/,
/<link[^>]*>([\s\S]*?)<\/link>/
);
}
return out;
}
/** 去除 CDATA、解码常见实体、压缩空白 */
function clean(s) {
return s
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'|'/g, "'")
.replace(/\s+/g, ' ')
.trim();
}