Add 'rss' bot: broadcasts an RSS/Atom feed to a channel

New rss bot type: on start it creates a broadcast channel (observer group with
recent history on) and polls a configured feed URL; new posts are broadcast to
the channel. Subscribers join the channel link (seen on the bot's profile);
direct contacts get a welcome + the latest items and can send /new for the
latest. Stdlib-only feed parsing (urllib + ElementTree), seeds existing items on
startup so it doesn't replay the whole feed. Config: feed_url, poll_seconds.
Adds rss_test.py (mock feed) — passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-05 21:08:05 +01:00
parent 12d21e6de5
commit 908d16bfc3
3 changed files with 302 additions and 2 deletions

View File

@@ -49,6 +49,7 @@
<table>
<tr><td><span class="tag">echo</span></td><td class="muted">Repeats every message back to the sender — handy for testing a connection end to end.</td></tr>
<tr><td><span class="tag">llm</span></td><td class="muted">Chat with a local or remote LLM (OpenAI-compatible, e.g. Ollama). Give it context, it replies to your messages.</td></tr>
<tr><td><span class="tag">rss</span></td><td class="muted">Watches an RSS/Atom feed and broadcasts new posts to a channel it creates. Subscribers join the channel; send /new for the latest.</td></tr>
<tr><td><span class="tag">broadcast</span></td><td class="muted">Relays messages from authorized publishers out to all of the bot's contacts.</td></tr>
<tr><td><span class="tag">support</span></td><td class="muted">Business inbox — auto-replies with a welcome message and collects incoming inquiries.</td></tr>
<tr><td><span class="tag">directory</span></td><td class="muted">Directory service for discovering and listing groups or contacts.</td></tr>
@@ -224,6 +225,23 @@
<input type="text" name="prohibited_message" placeholder="Only publishers can broadcast. Your message is deleted.">
</div>
</div>
<div id="rss-fields" style="display:none;">
<div style="border-top:1px solid var(--border);margin:4px 0 14px;padding-top:14px;">
<p class="muted" style="margin-bottom:12px;">
The bot watches this feed and broadcasts new posts to a channel it creates.
Share the channel link (from the bot's profile) with subscribers; anyone messaging
the bot can send <code>/new</code> for the latest.
</p>
</div>
<div class="field">
<label>Feed URL</label>
<input type="text" name="feed_url" placeholder="https://example.com/feed.xml">
</div>
<div class="field">
<label>Poll interval <span class="muted" style="font-weight:400;">(seconds)</span></label>
<input type="number" name="poll_seconds" min="30" value="300">
</div>
</div>
{% endif %}
<div class="flex gap-8 mt-16" style="justify-content:flex-end;">
<button type="button" class="btn btn-ghost"
@@ -297,6 +315,7 @@ function onTypeChange() {
document.getElementById('deadmans-fields').style.display = (val === 'deadmans') ? 'block' : 'none';
document.getElementById('directory-fields').style.display = (val === 'directory') ? 'block' : 'none';
document.getElementById('broadcast-fields').style.display = (val === 'broadcast') ? 'block' : 'none';
document.getElementById('rss-fields').style.display = (val === 'rss') ? 'block' : 'none';
}
{% endif %}
@@ -341,6 +360,13 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
const prohibited = (fd.get('prohibited_message') || '').trim();
if (prohibited) config.prohibited_message = prohibited;
}
if (botType === 'rss') {
const url = (fd.get('feed_url') || '').trim();
if (!url) { alert('Feed URL is required for an RSS bot'); return; }
config.feed_url = url;
const ps = parseInt(fd.get('poll_seconds'), 10);
if (!isNaN(ps) && ps >= 30) config.poll_seconds = ps;
}
{% endif %}
// Shared profile fields (users and bots)
const bio = (fd.get('bio') || '').trim();