Skip to content

Instantly share code, notes, and snippets.

@weskerty
Last active April 4, 2026 12:24
Show Gist options
  • Select an option

  • Save weskerty/bb70085cb067cea9ee9a2f5a3a9f8920 to your computer and use it in GitHub Desktop.

Select an option

Save weskerty/bb70085cb067cea9ee9a2f5a3a9f8920 to your computer and use it in GitHub Desktop.
rss post channel & chats... (need `NPM` plugin and send npm rss-parser )
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