diff --git a/apps/rssPush.js b/apps/rssPush.js index e69de29..862825b 100644 --- a/apps/rssPush.js +++ b/apps/rssPush.js @@ -0,0 +1,104 @@ +import configControl from '../lib/config/configControl.js'; +import rssTools from '../models/rss/rss.js'; +import path from 'path'; +import screenshot from '../lib/rss/screenshot.js'; +import fs from 'fs'; + +const rssCache = new Map(); + +export default class RssPlugin extends plugin { + constructor() { + super({ + name: 'crystelf RSS订阅', + dsc: '定时推送rss解析流', + rule: [ + { + reg: '^#rss添加(.+)$', + fnc: 'addFeed', + permission: 'master', + }, + { + reg: '^#rss移除(\\d+)$', + fnc: 'removeFeed', + permission: 'master', + }, + ], + task: [ + { + name: 'RSS定时推送', + corn: '*/10 * * * *', + fnc: () => this.pushFeeds(), + }, + ], + }); + } + + /** + * 添加rss + * @param e + * @returns {Promise<*>} + */ + async addFeed(e) { + const url = e.msg.replace(/^#rss添加/, '').trim(); + const feeds = configControl.get('feeds') || []; + const groupId = e.group_id; + + const exists = feeds.find((f) => f.url === url); + if (exists) { + if (!exists.targetGroups.includes(groupId)) { + exists.targetGroups.push(groupId); + await configControl.set('feeds', feeds); + return e.reply(`群已添加到该rss订阅中..`, true); + } + return e.reply(`该rss已存在并包含在该群聊..`, true); + } + feeds.push({ url, targetGroup: [groupId], screenshot: true }); + await configControl.set('feeds', feeds); + return e.reply(`rss解析流设置成功..`); + } + + /** + * 移除rss + * @param e + * @returns {Promise<*>} + */ + async removeFeed(e) { + const index = parseInt(e.msg.replace(/^#rss移除/, '').trim(), 10); + const feeds = configControl.get('feeds') || []; + const groupId = e.group_id; + + if (index < 0 || index >= feeds.length) return e.reply('索引无效'); + + feeds[index].targetGroups = feeds[index].targetGroups.filter((id) => id !== groupId); + await configControl.set('feeds', feeds); + return e.reply('群已移除该订阅'); + } + + /** + * 检查rss更新 + * @param e + * @returns {Promise} + */ + async pushFeeds(e) { + const feeds = configControl.get('feeds') || []; + for (const feed of feeds) { + const latest = await rssTools.fetchFeed(feed.url); + if (!latest || latest.length) continue; + const cacheKey = feed.url; + const lastId = rssCache.get(cacheKey); + const newItems = lastId ? latest.filter((i) => i.link !== lastId) : latest; + if (newItems.length) rssCache.set(cacheKey, newItems[0].link); + for (const groupId of feed.targetGroups) { + const post = newItems[0]; + const tempPath = path.join(process.cwd(), 'data', `rss-${Date.now()}.png`); + if (feed.screenshot) { + await screenshot.generateScreenshot(post, tempPath); + Bot.pickGroup(groupId)?.sendMsg([segment.image(tempPath)]); + fs.unlinkSync(tempPath); + } else { + Bot.pickGroup(groupId)?.sendMsg(`[RSS推送]\n${post.title}\n${post.link}`); + } + } + } + } +} diff --git a/constants/rss/rss_template.html b/constants/rss/rss_template.html index 235fe92..3a9e753 100644 --- a/constants/rss/rss_template.html +++ b/constants/rss/rss_template.html @@ -1,43 +1,37 @@ - - + - + -
+

{{title}}

-

{{description}}

-

{{link}}

+
作者:{{author}} | 时间:{{date}}
+
{{content}}
+
diff --git a/lib/rss/screenshot.js b/lib/rss/screenshot.js index 3d06b03..a60cd46 100644 --- a/lib/rss/screenshot.js +++ b/lib/rss/screenshot.js @@ -4,30 +4,29 @@ import puppeteer from 'puppeteer'; const screenshot = { /** - * 调用浏览器截图 - * @param title 标题 - * @param link 链接 - * @param description 描述 - * @returns {Promise} 图片所在目录 + * rss网页截图 + * @param feedItem 对象 + * @param savePath 保存路径 + * @returns {Promise<*>} */ - // TODO 待优化 - async rssScreenshot(title, link, description = '') { - const templatePath = paths.rssHTML; - let html = fs.readFileSync(templatePath, 'utf8'); + async generateScreenshot(feedItem, savePath) { + const htmlTemplate = fs.readFileSync(paths.rssHTML, 'utf-8'); + const html = htmlTemplate + .replace('{{title}}', feedItem.title) + .replace('{{author}}', feedItem.author) + .replace('{{content}}', feedItem.content) + .replace('{{link}}', feedItem.link) + .replace('{{date}}', new Date(feedItem.date).toLocaleString()) + .replace('{{feedTitle}}', feedItem.feedTitle) + .replace('{{image}}', feedItem.image || ''); - html = html - .replace('{{title}}', title || '') - .replace('{{link}}', link || '') - .replace('{{description}}', description || ''); - - const browser = await puppeteer.launch({ args: ['--no-sandbox'] }); + const browser = await puppeteer.launch({ headers: 'new' }); const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); - - const imagePath = `/tmp/rss_card_${Date.now()}.png`; - await page.screenshot({ path: imagePath }); + await page.setViewport({ width: 800, height: 600 }); + await page.screenshot({ path: savePath, fullPage: true }); await browser.close(); - return imagePath; + return savePath; }, }; diff --git a/models/rss/rss.js b/models/rss/rss.js new file mode 100644 index 0000000..85eb817 --- /dev/null +++ b/models/rss/rss.js @@ -0,0 +1,39 @@ +import Parser from 'rss-parser'; + +const parser = new Parser(); +//去掉不干净的东西 +const cleanHTML = (html) => { + return html + .replace(/该渲染由.*?<\/blockquote>/gs, '') + .replace(/[\s\S]*?<\/script>/gi, '') + .replace(/[\s\S]*?<\/style>/gi, ''); +}; + +const rssTools = { + /** + * 拉取rss + * 已适配Atom&RSS2.0 + * @param url rss地址 + * @returns {Promise<{title: *, link: *, content: *, author, date, feedTitle: string, feedLink: string, image}[]|null>} + */ + async fetchFeed(url) { + try { + const feed = await parser.parseURL(url); + return feed.items.map((item) => ({ + title: item.title, + link: item.link, + content: cleanHTML(item['content:encoded'] || item.content || item.description || ''), + author: item.creator || item.author || feed.title, + date: item.pubDate || item.isoDate, + feedTitle: feed.title, + feedLink: feed.link, + image: feed.image?.url || '', + })); + } catch (err) { + logger.error(`RSS 拉取失败: ${url}`, err); + return null; + } + }, +}; + +export default rssTools;