fix(telegram): split long messages at 4096 chars to avoid truncation

- Add TELEGRAM_MAX_TEXT and _chunkText(); send multiple messages when over limit
- Prefer newline boundaries to avoid breaking Markdown

Made-with: Cursor
This commit is contained in:
satoshipanic
2026-03-17 14:04:32 +01:00
committed by satoshipanic
parent 2a0b73e5a6
commit 28121298cf

View File

@@ -4,6 +4,8 @@
import { createHash } from 'crypto';
const TELEGRAM_API = 'https://api.telegram.org';
/** Telegram Bot API limit for sendMessage text (bytes/characters). */
const TELEGRAM_MAX_TEXT = 4096;
// ─── Alert Tiers ────────────────────────────────────────────────────────────
// FLASH: Immediate action required — market-moving, time-critical (e.g. war escalation, flash crash)
@@ -48,42 +50,70 @@ export class TelegramAlerter {
// ─── Core Messaging ─────────────────────────────────────────────────────
/**
* Send a message via Telegram Bot API.
* Send a message via Telegram Bot API. Splits at TELEGRAM_MAX_TEXT so long messages
* (e.g. /brief) are sent in multiple messages instead of being truncated or failing.
* @param {string} message - markdown-formatted message
* @param {object} opts - optional: { parseMode, disablePreview, replyToMessageId }
* @param {object} opts - optional: { parseMode, disablePreview, replyToMessageId, chatId }
* @returns {Promise<{ok: boolean, messageId?: number}>}
*/
async sendMessage(message, opts = {}) {
if (!this.isConfigured) return { ok: false };
const chatId = opts.chatId ?? this.chatId;
const parseMode = opts.parseMode || 'Markdown';
const chunks = this._chunkText(message, TELEGRAM_MAX_TEXT);
try {
const res = await fetch(`${TELEGRAM_API}/bot${this.botToken}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: opts.chatId ?? this.chatId,
text: message,
parse_mode: opts.parseMode || 'Markdown',
disable_web_page_preview: opts.disablePreview !== false,
...(opts.replyToMessageId ? { reply_to_message_id: opts.replyToMessageId } : {}),
}),
signal: AbortSignal.timeout(15000),
});
let lastResult = { ok: false, messageId: undefined };
for (let i = 0; i < chunks.length; i++) {
const res = await fetch(`${TELEGRAM_API}/bot${this.botToken}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: chunks[i],
parse_mode: parseMode,
disable_web_page_preview: opts.disablePreview !== false,
...(opts.replyToMessageId && i === 0 ? { reply_to_message_id: opts.replyToMessageId } : {}),
}),
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const err = await res.text().catch(() => '');
console.error(`[Telegram] Send failed (${res.status}): ${err.substring(0, 200)}`);
return { ok: false };
if (!res.ok) {
const err = await res.text().catch(() => '');
console.error(`[Telegram] Send failed (${res.status}): ${err.substring(0, 200)}`);
return lastResult;
}
const data = await res.json();
lastResult = { ok: true, messageId: data.result?.message_id };
}
const data = await res.json();
return { ok: true, messageId: data.result?.message_id };
return lastResult;
} catch (err) {
console.error('[Telegram] Send error:', err.message);
return { ok: false };
}
}
/**
* Split text into chunks of at most maxLen. Prefer breaking at newlines to avoid
* splitting mid-Markdown.
*/
_chunkText(text, maxLen = TELEGRAM_MAX_TEXT) {
if (!text || text.length <= maxLen) return text ? [text] : [];
const chunks = [];
let start = 0;
while (start < text.length) {
let end = Math.min(start + maxLen, text.length);
if (end < text.length) {
const lastNewline = text.lastIndexOf('\n', end - 1);
if (lastNewline > start) end = lastNewline + 1;
}
chunks.push(text.slice(start, end));
start = end;
}
return chunks;
}
// Backward-compatible alias
async sendAlert(message) {
const result = await this.sendMessage(message);