/** * Telegram — the real L6 "Comms" layer, in ~60 lines via the raw Bot API (no * dependency). Outbound: tgSend() broadcasts signals/closes. Inbound: tgPoll() * long-polls getUpdates for slash commands (/status, /pnl, /positions, /help). * * If no token/chat is configured the worker still runs — messages print to stdout * so you can demo the whole loop with zero Telegram setup. */ import { CONFIG } from './config.ts' const api = (method: string) => `https://api.telegram.org/bot${CONFIG.tgToken}/${method}` export const TG_ENABLED = Boolean(CONFIG.tgToken && CONFIG.tgChat) const stripHtml = (s: string) => s.replace(/<[^>]+>/g, '') /** Send a message (HTML). Defaults to the configured chat; pass chatId to reply. */ export async function tgSend(text: string, chatId: string | number = CONFIG.tgChat): Promise { if (!CONFIG.tgToken || !chatId) { console.log('\n' + stripHtml(text) + '\n') return } try { const res = await fetch(api('sendMessage'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: chatId, text, parse_mode: 'HTML', disable_web_page_preview: true, }), }) if (!res.ok) console.error('[tg] send failed', res.status, await res.text()) } catch (e) { console.error('[tg] send error', (e as Error).message) } } let offset = 0 /** Poll once for new commands and dispatch them. Cheap to call on an interval. */ export async function tgPoll( handler: (cmd: string, chatId: number) => Promise, ): Promise { if (!CONFIG.tgToken) return try { const res = await fetch(api('getUpdates') + `?timeout=0&offset=${offset}`) if (!res.ok) return const json = (await res.json()) as { result?: { update_id: number; message?: { text?: string; chat?: { id: number } } }[] } for (const u of json.result ?? []) { offset = u.update_id + 1 const text = u.message?.text const chatId = u.message?.chat?.id if (text && chatId != null && text.startsWith('/')) { // strip @BotName suffix and arguments → bare command const cmd = text.trim().split(/\s+/)[0].split('@')[0].toLowerCase() await handler(cmd, chatId) } } } catch { /* transient — ignore, next poll retries */ } }