feat: complete Telegram-compatible Bot API + bot dev guide

Bot API (routes/bot.rs — full rewrite):
- getUpdates: persistent update_id counter, offset acknowledgement,
  limit (max 100), long-poll up to 30s with 1s intervals
- sendMessage: parse_mode, reply_to_message_id, reply_markup (inline keyboards)
- answerCallbackQuery: acknowledge button clicks
- editMessageText: update sent messages
- setWebhook / deleteWebhook / getWebhookInfo: webhook configuration
- sendDocument: file reference with caption
- Bot queue: raw messages migrated to bot_queue:<fp>:<update_id> for ordering

Web client (routes/web.rs):
- Bot messages rendered properly (was showing "[message could not be decrypted]")
- Handles bot_message, bot_edit, bot_document as both Text and Binary WS frames
- Inline keyboard buttons rendered as bracketed text
- Missed call notifications handled in Text frame path

Docs:
- LLM_BOT_DEV.md: token-optimized bot dev reference for coding assistant LLM
  (Python + Node.js examples, all endpoints, TG compatibility table)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 07:50:14 +04:00
parent 953b3bd13a
commit fcbf2d5859
3 changed files with 829 additions and 169 deletions

View File

@@ -527,6 +527,45 @@ function connectWebSocket() {
};
ws.onmessage = async (event) => {
if (typeof event.data === 'string') {
// Text frame — could be a bot message or missed call notification
try {
const json = JSON.parse(event.data);
if (json.type === 'missed_call') {
addSys('Missed call from ' + (json.data?.caller_fp || 'unknown'));
return;
}
if (json.type === 'bot_message') {
const botName = json.from_name || json.from || 'bot';
let msgText = json.text || '';
if (json.reply_markup && json.reply_markup.inline_keyboard) {
msgText += '\\n';
for (const row of json.reply_markup.inline_keyboard) {
for (const btn of row) {
msgText += ' [' + btn.text + '] ';
}
msgText += '\\n';
}
}
addMsg('@' + botName, msgText, false);
lastDmPeer = json.from ? normFP(json.from) : '';
return;
}
if (json.type === 'bot_edit') {
addSys('[bot updated: ' + (json.text || '') + ']');
return;
}
if (json.type === 'bot_document') {
addMsg('@' + (json.from || 'bot'), '[Document: ' + json.document + ']', false);
return;
}
} catch(e) {}
// If not JSON or unrecognized, try treating as binary
const bytes = new TextEncoder().encode(event.data);
dbg('WS text frame treated as bytes,', bytes.length, 'bytes');
await handleIncomingMessage(bytes);
return;
}
const bytes = new Uint8Array(event.data);
dbg('WS received', bytes.length, 'bytes');
await handleIncomingMessage(bytes);
@@ -628,12 +667,39 @@ async function handleIncomingMessage(bytes) {
}
}
// Last try: raw JSON file messages (from web file upload)
// Last try: raw JSON file messages (from web file upload) or bot messages
try {
const str = new TextDecoder().decode(bytes);
const json = JSON.parse(str);
if (json.type === 'file_header') { handleFileHeader(json); return; }
if (json.type === 'file_chunk') { handleFileChunk(json); return; }
// Handle bot messages (plaintext JSON from bot API)
if (json.type === 'bot_message') {
const botName = json.from_name || json.from || 'bot';
let msgText = json.text || '';
// Handle inline keyboard if present
if (json.reply_markup && json.reply_markup.inline_keyboard) {
msgText += '\\n';
for (const row of json.reply_markup.inline_keyboard) {
for (const btn of row) {
msgText += ' [' + btn.text + '] ';
}
msgText += '\\n';
}
}
addMsg('@' + botName, msgText, false);
lastDmPeer = json.from ? normFP(json.from) : '';
return;
}
if (json.type === 'bot_edit') {
addSys('[bot updated message: ' + (json.text || '') + ']');
return;
}
if (json.type === 'bot_document') {
const caption = json.caption ? ' \u2014 ' + json.caption : '';
addMsg('@' + (json.from || 'bot'), '[Document: ' + json.document + caption + ']', false);
return;
}
} catch(e) {}
dbg('ALL decrypt attempts failed');