Add 'crypto' bot: streams CoinGecko prices to a channel

New crypto bot type: creates a broadcast channel and posts a price snapshot of the
selected coins/currencies (CoinGecko simple/price JSON) every interval — same
channel-streaming model as RSS. Create form has checkbox grids for popular coins
and currencies plus a poll interval. Generalize the channel helper and feed-poll
state (channel_gid/poll_next) shared by rss + crypto. Adds crypto_test.py (mock
CoinGecko) — passes; rss_test updated for the renamed field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jon
2026-06-05 22:31:44 +01:00
parent 7cda767408
commit 3456ed9411
4 changed files with 282 additions and 29 deletions

View File

@@ -19,6 +19,11 @@
.bot-types-card table td { vertical-align: top; }
.bot-types-card .tag { white-space: nowrap; }
.chk-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 6px 12px; }
.chk { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 500;
color: var(--text); cursor: pointer; }
.chk input { width: auto; }
</style>
{% endblock %}
@@ -50,6 +55,7 @@
<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 to receive them.</td></tr>
<tr><td><span class="tag">crypto</span></td><td class="muted">Streams selected crypto prices (CoinGecko) to a channel on an interval. Pick coins &amp; currencies below.</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>
@@ -242,6 +248,45 @@
<input type="number" name="poll_seconds" min="30" value="300">
</div>
</div>
<div id="crypto-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;">
Posts a price snapshot of the selected coins to a channel every interval (via CoinGecko).
</p>
</div>
<div class="field">
<label>Coins</label>
<div class="chk-grid">
<label class="chk"><input type="checkbox" name="coin" value="bitcoin" checked> Bitcoin</label>
<label class="chk"><input type="checkbox" name="coin" value="ethereum" checked> Ethereum</label>
<label class="chk"><input type="checkbox" name="coin" value="solana"> Solana</label>
<label class="chk"><input type="checkbox" name="coin" value="ripple"> XRP</label>
<label class="chk"><input type="checkbox" name="coin" value="cardano"> Cardano</label>
<label class="chk"><input type="checkbox" name="coin" value="dogecoin"> Dogecoin</label>
<label class="chk"><input type="checkbox" name="coin" value="binancecoin"> BNB</label>
<label class="chk"><input type="checkbox" name="coin" value="polkadot"> Polkadot</label>
<label class="chk"><input type="checkbox" name="coin" value="litecoin"> Litecoin</label>
<label class="chk"><input type="checkbox" name="coin" value="tron"> TRON</label>
<label class="chk"><input type="checkbox" name="coin" value="chainlink"> Chainlink</label>
<label class="chk"><input type="checkbox" name="coin" value="tether"> Tether</label>
</div>
</div>
<div class="field">
<label>Currencies</label>
<div class="chk-grid">
<label class="chk"><input type="checkbox" name="cur" value="usd" checked> USD</label>
<label class="chk"><input type="checkbox" name="cur" value="gbp" checked> GBP</label>
<label class="chk"><input type="checkbox" name="cur" value="eur"> EUR</label>
<label class="chk"><input type="checkbox" name="cur" value="jpy"> JPY</label>
<label class="chk"><input type="checkbox" name="cur" value="aud"> AUD</label>
<label class="chk"><input type="checkbox" name="cur" value="cad"> CAD</label>
</div>
</div>
<div class="field">
<label>Poll interval <span class="muted" style="font-weight:400;">(seconds)</span></label>
<input type="number" name="crypto_poll_seconds" min="60" value="300">
</div>
</div>
{% endif %}
<div class="flex gap-8 mt-16" style="justify-content:flex-end;">
<button type="button" class="btn btn-ghost"
@@ -330,6 +375,7 @@ function onTypeChange() {
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';
document.getElementById('crypto-fields').style.display = (val === 'crypto') ? 'block' : 'none';
}
{% endif %}
@@ -381,6 +427,16 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
const ps = parseInt(fd.get('poll_seconds'), 10);
if (!isNaN(ps) && ps >= 30) config.poll_seconds = ps;
}
if (botType === 'crypto') {
const coins = Array.from(document.querySelectorAll('#crypto-fields input[name=coin]:checked')).map(c => c.value);
const curs = Array.from(document.querySelectorAll('#crypto-fields input[name=cur]:checked')).map(c => c.value);
if (!coins.length) { alert('Pick at least one coin'); return; }
if (!curs.length) { alert('Pick at least one currency'); return; }
config.coins = coins;
config.currencies = curs;
const ps = parseInt(fd.get('crypto_poll_seconds'), 10);
if (!isNaN(ps) && ps >= 60) config.poll_seconds = ps;
}
{% endif %}
// Shared profile fields (users and bots)
const bio = (fd.get('bio') || '').trim();