Last active
April 4, 2026 12:24
-
-
Save weskerty/bb70085cb067cea9ee9a2f5a3a9f8920 to your computer and use it in GitHub Desktop.
rss post channel & chats... (need `NPM` plugin and send npm rss-parser )
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const fs = require('fs').promises; | |
| const path = require('path'); | |
| const { bot } = require('../lib'); | |
| class RSSManager { | |
| constructor() { | |
| this.client = null; | |
| this.dataDir = null; | |
| this.intervalMs = 60000; | |
| this.interval = null; | |
| this.polling = false; | |
| this.parser = null; | |
| } | |
| setup(client, ctx) { | |
| this.client = client; | |
| if (!this.dataDir) | |
| this.dataDir = path.join(ctx.TIENDA_BASE ? path.dirname(ctx.TIENDA_BASE) : process.cwd(), 'media', 'rss'); | |
| if (!this.interval) { | |
| this.intervalMs = parseInt(ctx.RSS_INTERVAL, 10) || 60000; | |
| this.interval = setInterval(() => this.pollAll(), this.intervalMs); | |
| } | |
| } | |
| getParser() { | |
| if (!this.parser) { | |
| const Parser = require('rss-parser'); | |
| this.parser = new Parser({ timeout: 10000, customFields: { item: ['author', 'description'] } }); | |
| } | |
| return this.parser; | |
| } | |
| async loadJ(p, def) { | |
| try { return JSON.parse(await fs.readFile(p, 'utf8')); } catch { return def; } | |
| } | |
| async saveJ(p, d) { | |
| await fs.mkdir(path.dirname(p), { recursive: true }); | |
| await fs.writeFile(p, JSON.stringify(d, null, 2), 'utf8'); | |
| } | |
| catFromMime(mime) { | |
| if (!mime) return null; | |
| if (mime.startsWith('video/')) return 'video'; | |
| if (mime.startsWith('image/')) return 'image'; | |
| if (mime.startsWith('audio/')) return 'audio'; | |
| return null; | |
| } | |
| mimeFromExt(url) { | |
| const ext = (url.split('?')[0].split('.').pop() || '').toLowerCase(); | |
| const map = { mp4:'video/mp4', webm:'video/webm', mov:'video/mp4', mkv:'video/mp4', avi:'video/mp4', jpg:'image/jpeg', jpeg:'image/jpeg', png:'image/png', gif:'image/gif', webp:'image/webp', mp3:'audio/mpeg', ogg:'audio/ogg', wav:'audio/wav', m4a:'audio/mpeg', aac:'audio/mpeg', flac:'audio/mpeg' }; | |
| return map[ext] || null; | |
| } | |
| extractMedia(html) { | |
| const src = html.match(/<source[^>]+src=["']([^"']+)["'][^>]*(?:type=["']([^"']+)["'])?/i); | |
| if (src) return { url: src[1], hint: src[2] || null }; | |
| const vid = html.match(/<video[^>]+src=["']([^"']+)["']/i); | |
| if (vid) return { url: vid[1], hint: 'video/mp4' }; | |
| const aud = html.match(/<audio[^>]+src=["']([^"']+)["']/i); | |
| if (aud) return { url: aud[1], hint: 'audio/mpeg' }; | |
| const img = html.match(/<img[^>]+src=["']([^"']+)["']/i); | |
| if (img) return { url: img[1], hint: 'image/jpeg' }; | |
| return null; | |
| } | |
| cleanHtml(html) { | |
| return html | |
| .replace(/<br\s*\/?>/gi, '\n') | |
| .replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, txt) => { | |
| const t = txt.replace(/<[^>]+>/g, '').trim(); | |
| return t ? `${t} ${href}` : href; | |
| }) | |
| .replace(/<[^>]+>/g, ' ') | |
| .replace(/[ \t]+/g, ' ') | |
| .replace(/\n{3,}/g, '\n\n') | |
| .trim() | |
| .slice(0, 400); | |
| } | |
| itemId(i) { return i.guid || i.link || i.id || ''; } | |
| itemText(item, feedTitle) { | |
| const html = item.content || item['content:encoded'] || item.description || ''; | |
| const desc = this.cleanHtml(html) || (item.contentSnippet || '').trim().slice(0, 400); | |
| const link = item.link || ''; | |
| const author = item.author || item.creator || feedTitle || ''; | |
| let msg = ''; | |
| if (item.title) msg += `*${item.title}*\n`; | |
| if (desc) msg += `${desc}\n`; | |
| if (author || link) msg += `> ${author}${author && link ? ' ' : ''}${link}`; | |
| return msg.trim(); | |
| } | |
| async sendItem(jid, item, feedTitle) { | |
| const html = item.content || item['content:encoded'] || item.description || ''; | |
| const caption = this.itemText(item, feedTitle); | |
| let media = null; | |
| if (item.enclosure?.url) media = { url: item.enclosure.url, hint: item.enclosure.type || null }; | |
| else if (item['media:content']?.['$']?.url) media = { url: item['media:content']['$'].url, hint: item['media:content']['$'].type || null }; | |
| else if (item['media:thumbnail']?.['$']?.url) media = { url: item['media:thumbnail']['$'].url, hint: 'image/jpeg' }; | |
| else media = this.extractMedia(html); | |
| if (media) { | |
| const tmpPath = `/tmp/rss_${Date.now()}`; | |
| try { | |
| const r = await fetch(media.url); | |
| if (r.ok) { | |
| const ct = r.headers.get('content-type')?.split(';')[0].trim(); | |
| const mime = (ct && ct !== 'application/octet-stream') ? ct : (media.hint || this.mimeFromExt(media.url) || 'image/jpeg'); | |
| const cat = this.catFromMime(mime) || 'image'; | |
| await fs.writeFile(tmpPath, Buffer.from(await r.arrayBuffer())); | |
| const buf = await fs.readFile(tmpPath); | |
| if (cat === 'video') await this.client.sendMessage(jid, { video: buf, caption, mimetype: mime }); | |
| else if (cat === 'audio') await this.client.sendMessage(jid, { audio: buf, caption, mimetype: mime, ptt: mime === 'audio/ogg' }); | |
| else await this.client.sendMessage(jid, { image: buf, caption }); | |
| return; | |
| } | |
| } catch {} | |
| finally { await fs.unlink(tmpPath).catch(() => {}); } | |
| } | |
| await this.client.sendMessage(jid, { text: caption }); | |
| } | |
| async checkFeed(jid, url, seen) { | |
| const key = `${jid}||${url}`; | |
| const feed = await this.getParser().parseURL(url); | |
| const items = feed.items || []; | |
| const curIds = items.map(i => this.itemId(i)).filter(Boolean); | |
| if (!seen[key]) { seen[key] = curIds; return { feed, dirty: true }; } | |
| const newItems = items.filter(i => { const id = this.itemId(i); return id && !seen[key].includes(id); }); | |
| let dirty = false; | |
| for (const item of newItems.reverse()) { | |
| try { await this.sendItem(jid, item, feed.title); } catch {} | |
| const id = this.itemId(item); | |
| if (id) { seen[key].push(id); dirty = true; } | |
| } | |
| if (seen[key].length > 500) seen[key] = seen[key].slice(-200); | |
| return { feed, dirty }; | |
| } | |
| async pollAll() { | |
| if (!this.client || !this.dataDir || this.polling) return; | |
| this.polling = true; | |
| try { | |
| const subsPath = path.join(this.dataDir, 'subs.json'); | |
| const seenPath = path.join(this.dataDir, 'seen.json'); | |
| const subs = await this.loadJ(subsPath, {}); | |
| let seen = await this.loadJ(seenPath, {}); | |
| let dirty = false; | |
| for (const [jid, urls] of Object.entries(subs)) { | |
| if (!urls.length) continue; | |
| for (const url of urls) { | |
| try { const r = await this.checkFeed(jid, url, seen); if (r.dirty) dirty = true; } catch {} | |
| } | |
| } | |
| if (dirty) await this.saveJ(seenPath, seen); | |
| } finally { | |
| this.polling = false; | |
| } | |
| } | |
| } | |
| const rss = new RSSManager(); | |
| bot({ | |
| on: 'message', | |
| type: 'rss_init' | |
| }, async (message, match, ctx) => { | |
| if (!rss.interval) rss.setup(message.client, ctx); | |
| }); | |
| bot({ | |
| pattern: 'rss ?(.*)', | |
| fromMe: true, | |
| desc: 'RSS', | |
| type: 'rss' | |
| }, async (message, match, ctx) => { | |
| if (!rss.interval) rss.setup(message.client, ctx); | |
| const subsPath = path.join(rss.dataDir, 'subs.json'); | |
| const seenPath = path.join(rss.dataDir, 'seen.json'); | |
| const jid = message.jid; | |
| const arg = (match || '').trim(); | |
| const reply = t => message.send(t, { quoted: message.data }); | |
| const subs = await rss.loadJ(subsPath, {}); | |
| if (!subs[jid]) subs[jid] = []; | |
| if (!arg || arg === 'list') { | |
| if (!subs[jid].length) return reply('Sin suscripciones.'); | |
| return reply('RSS activos:\n' + subs[jid].map((u, i) => `${i + 1}. ${u}`).join('\n')); | |
| } | |
| if (arg.startsWith('off ') || arg.startsWith('del ')) { | |
| const url = arg.slice(4).trim(); | |
| const before = subs[jid].length; | |
| subs[jid] = subs[jid].filter(u => u !== url); | |
| await rss.saveJ(subsPath, subs); | |
| return reply(subs[jid].length < before ? `Desuscrito: ${url}` : 'URL no encontrada.'); | |
| } | |
| const url = arg; | |
| const alreadySub = subs[jid].includes(url); | |
| try { | |
| let seen = await rss.loadJ(seenPath, {}); | |
| const { feed } = await rss.checkFeed(jid, url, seen); | |
| if (!alreadySub) { subs[jid].push(url); await rss.saveJ(subsPath, subs); } | |
| await rss.saveJ(seenPath, seen); | |
| await reply(`Suscrito: *${feed.title || url}*`); | |
| for (const item of (feed.items || []).slice(0, 3).reverse()) { | |
| try { await rss.sendItem(jid, item, feed.title); } catch {} | |
| } | |
| } catch (e) { reply(`Error: ${e.message}`); } | |
| }); | |
| module.exports = { rss }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment