v0.0.27: TG-compatible bots — plaintext send, numeric IDs, webhooks, BotFather

Bot compatibility:
- Clients send plaintext bot_message to bot aliases (no E2E encryption)
- Numeric chat_id: fp_to_numeric_id() deterministic hash, accept string/number
- Webhook delivery: POST updates to bot's webhook URL (async, fire-and-forget)
- getUpdates timeout raised to 50s (was 30, TG uses 50)
- parse_mode HTML rendered in web client
- E2E bot registration: optional seed + bundle for encrypted bot sessions

BotFather + instance control:
- --enable-bots CLI flag (default: disabled)
- BotFather auto-created on first start (@botfather alias)
- Bot ownership: owner fingerprint stored in bot_info
- All bot endpoints return 403 when disabled

Bot Bridge:
- tools/bot-bridge.py: TG-compatible proxy for unmodified TG bots
- Translates chat_id int↔string, proxies getUpdates/sendMessage
- README with python-telegram-bot and Telegraf examples

Test fixes:
- Updated tests for ETH address display in header/messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 09:45:45 +04:00
parent 067f1ea20b
commit 8603087afb
14 changed files with 660 additions and 120 deletions

View File

@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
async fn service_worker() -> impl IntoResponse {
([(header::CONTENT_TYPE, "application/javascript")], r##"
const CACHE = 'wz-v7';
const CACHE = 'wz-v9';
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
self.addEventListener('install', e => {
@@ -241,7 +241,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.25';
const VERSION = '0.0.27';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──
@@ -547,7 +547,8 @@ function connectWebSocket() {
msgText += '\\n';
}
}
addMsg('@' + botName, msgText, false);
const useHtml = json.parse_mode === 'HTML';
addMsg('@' + botName, msgText, false, null, useHtml);
lastDmPeer = json.from ? normFP(json.from) : '';
return;
}
@@ -693,7 +694,8 @@ async function handleIncomingMessage(bytes) {
msgText += '\\n';
}
}
addMsg('@' + botName, msgText, false);
const useHtml = json.parse_mode === 'HTML';
addMsg('@' + botName, msgText, false, null, useHtml);
lastDmPeer = json.from ? normFP(json.from) : '';
return;
}
@@ -801,7 +803,7 @@ function formatSize(n) {
return (n/1048576).toFixed(1) + ' MB';
}
function addMsg(from, text, isSelf, messageId) {
function addMsg(from, text, isSelf, messageId, rawHtml) {
const d = document.createElement('div');
d.className = 'msg';
const color = isSelf ? '#4ade80' : peerColor(from);
@@ -811,7 +813,8 @@ function addMsg(from, text, isSelf, messageId) {
const status = (sentMsgReceipts[messageId] && sentMsgReceipts[messageId].status) || 'sent';
receiptHtml = ' <span class="receipt" style="color:' + receiptColor(status) + '"> ' + receiptIndicator(status) + '</span>';
}
d.innerHTML = '<span class="ts">' + ts() + '</span> ' + lock + '<span style="color:' + color + ';font-weight:bold">' + makeAddressClickable(esc(from)) + '</span>: ' + makeAddressClickable(esc(text)) + receiptHtml;
const bodyHtml = rawHtml ? text : makeAddressClickable(esc(text));
d.innerHTML = '<span class="ts">' + ts() + '</span> ' + lock + '<span style="color:' + color + ';font-weight:bold">' + makeAddressClickable(esc(from)) + '</span>: ' + bodyHtml + receiptHtml;
// Attach click handler for .addr spans
d.querySelectorAll('.addr').forEach(el => {
el.addEventListener('click', () => handleAddrClick(el.dataset.addr));
@@ -1202,6 +1205,22 @@ async function doSend() {
localStorage.setItem('wz-peer', $peerInput.value.trim());
// Check if peer is a bot — send plaintext instead of E2E
let isBotPeer = false;
try {
const wr = await fetch(SERVER + '/v1/alias/whois/' + normFP(peer));
const wd = await wr.json();
if (wd.alias && (wd.alias.endsWith('bot') || wd.alias.endsWith('Bot') || wd.alias.endsWith('_bot'))) isBotPeer = true;
} catch(e) {}
if (isBotPeer) {
const msgId = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
const botMsg = {type:'bot_message',id:msgId,from:normFP(myFingerprint),from_name:myEthAddress||myFingerprint.slice(0,19),text:text,timestamp:Math.floor(Date.now()/1000)};
await fetch(SERVER+'/v1/messages/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:normFP(peer),from:normFP(myFingerprint),message:Array.from(new TextEncoder().encode(JSON.stringify(botMsg)))})});
addMsg((myEthAddress ? myEthAddress.slice(0,12)+'...' : myFingerprint.slice(0,19)), text, true, msgId);
return;
}
try {
const msgId = await sendEncrypted(peer, text);
sentMsgReceipts[msgId] = { status: 'sent', el: null };