| 45 | async stop(): Promise<void> { this.running = false; } |
| 46 | |
| 47 | private async loop(onMessage: (m: Incoming) => void): Promise<void> { |
| 48 | while (this.running) { |
| 49 | try { |
| 50 | const res = await this.api('getUpdates', { offset: this.offset, timeout: 30, allowed_updates: ['message', 'callback_query'] }); |
| 51 | if (!res.ok) { await sleep(2000); continue; } |
| 52 | for (const u of res.result) { |
| 53 | this.offset = u.update_id + 1; |
| 54 | if (u.message?.text) { |
| 55 | onMessage({ platform: 'telegram', chatId: String(u.message.chat.id), userId: String(u.message.from?.id), userName: u.message.from?.username, text: u.message.text }); |
| 56 | } else if (u.message?.voice || u.message?.audio) { |
| 57 | // Voice memo → transcribe (local-first) → feed as text. Fire-and-forget so the |
| 58 | // poll loop keeps flowing; failures reply with a note instead of crashing. |
| 59 | void this.handleVoice(u.message, onMessage); |
| 60 | } else if (u.callback_query) { |
| 61 | const cq = u.callback_query; |
| 62 | onMessage({ platform: 'telegram', chatId: String(cq.message?.chat?.id), userId: String(cq.from?.id), text: '', callbackData: cq.data, callbackId: cq.id }); |
| 63 | } |
| 64 | } |
| 65 | } catch (e: any) { |
| 66 | logger.warn('telegram poll error', { err: e?.message }); |
| 67 | await sleep(2000); |
| 68 | } |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | async send(chatId: string, text: string, buttons?: Button[][]): Promise<MessageRef> { |
| 73 | const r = await this.api('sendMessage', { chat_id: chatId, text, disable_web_page_preview: true, ...this.keyboard(buttons) }); |