Broadcast bot: parity with official simplex-broadcast-bot

Relay publishers' text/links to all contacts via the native /feed command
(reports 'Forwarded to N contact(s), M errors'); reply to non-publishers with
the prohibited message and internally delete their message (CIDMInternal, as
upstream does). Filter content to text/links. Publishers accept 'Name' or
'ID:Name'; welcome/prohibited defaults list the publishers. Add publishers +
prohibited-reply fields to the create form. Adds broadcast_test.py (3 in-process
controllers: bot + publisher + subscriber) — passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-05 16:27:56 +01:00
parent c1bb9cb955
commit 6232c1589d
3 changed files with 274 additions and 23 deletions

View File

@@ -183,6 +183,22 @@
<input type="text" name="superusers" placeholder="Alice, Bob">
</div>
</div>
<div id="broadcast-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;">
Only listed publishers can broadcast; their text/links are relayed to every contact.
Anyone else gets the prohibited reply and their message is deleted.
</p>
</div>
<div class="field">
<label>Publishers <span class="muted" style="font-weight:400;">(comma-separated; "Name" or "ID:Name")</span></label>
<input type="text" name="publishers" placeholder="Alice, 2:Bob">
</div>
<div class="field">
<label>Prohibited reply <span class="muted" style="font-weight:400;">(blank = default listing publishers)</span></label>
<input type="text" name="prohibited_message" placeholder="Only publishers can broadcast. Your message is deleted.">
</div>
</div>
{% endif %}
<div class="flex gap-8 mt-16" style="justify-content:flex-end;">
<button type="button" class="btn btn-ghost"
@@ -255,6 +271,7 @@ function onTypeChange() {
document.getElementById('support-fields').style.display = (val === 'support') ? 'block' : 'none';
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';
}
{% endif %}
@@ -293,6 +310,12 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
const su = (fd.get('superusers') || '').split(',').map(s => s.trim()).filter(Boolean);
if (su.length) config.superusers = su;
}
if (botType === 'broadcast') {
const pubs = (fd.get('publishers') || '').split(',').map(s => s.trim()).filter(Boolean);
if (pubs.length) config.publishers = pubs;
const prohibited = (fd.get('prohibited_message') || '').trim();
if (prohibited) config.prohibited_message = prohibited;
}
{% endif %}
// Shared profile fields (users and bots)
const bio = (fd.get('bio') || '').trim();