fix(telegram): register slash commands and support DM/group two-way bot
- Call setMyCommands on startup for private and group chat scopes - Parse /cmd@BotName in groups; reply to originating chat - Allow sendMessage chatId override for command replies Made-with: Cursor
This commit is contained in:
committed by
satoshipanic
parent
0200e6d9d5
commit
2a0b73e5a6
@@ -38,6 +38,7 @@ export class TelegramAlerter {
|
||||
this._lastUpdateId = 0; // For polling bot commands
|
||||
this._commandHandlers = {}; // Registered command callbacks
|
||||
this._pollingInterval = null;
|
||||
this._botUsername = null;
|
||||
}
|
||||
|
||||
get isConfigured() {
|
||||
@@ -60,7 +61,7 @@ export class TelegramAlerter {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chat_id: this.chatId,
|
||||
chat_id: opts.chatId ?? this.chatId,
|
||||
text: message,
|
||||
parse_mode: opts.parseMode || 'Markdown',
|
||||
disable_web_page_preview: opts.disablePreview !== false,
|
||||
@@ -286,6 +287,9 @@ export class TelegramAlerter {
|
||||
if (this._pollingInterval) return; // Already polling
|
||||
|
||||
console.log('[Telegram] Bot command polling started');
|
||||
this._initializeBotCommands().catch((err) => {
|
||||
console.error('[Telegram] Command initialization failed:', err.message);
|
||||
});
|
||||
this._pollingInterval = setInterval(() => this._pollUpdates(), intervalMs);
|
||||
// Initial poll
|
||||
this._pollUpdates();
|
||||
@@ -325,9 +329,10 @@ export class TelegramAlerter {
|
||||
const msg = update.message;
|
||||
if (!msg?.text) continue;
|
||||
|
||||
// Only process messages from the configured chat
|
||||
const chatType = msg.chat?.type;
|
||||
const chatId = String(msg.chat?.id);
|
||||
if (chatId !== String(this.chatId)) continue;
|
||||
// Commands can come from private chats or the configured chat/group.
|
||||
if (chatType !== 'private' && chatId !== String(this.chatId)) continue;
|
||||
|
||||
await this._handleMessage(msg);
|
||||
}
|
||||
@@ -342,8 +347,11 @@ export class TelegramAlerter {
|
||||
async _handleMessage(msg) {
|
||||
const text = msg.text.trim();
|
||||
const parts = text.split(/\s+/);
|
||||
const command = parts[0].toLowerCase();
|
||||
const rawCommand = parts[0].toLowerCase();
|
||||
const command = this._normalizeCommand(rawCommand);
|
||||
if (!command) return;
|
||||
const args = parts.slice(1).join(' ');
|
||||
const replyChatId = msg.chat?.id;
|
||||
|
||||
// Built-in commands
|
||||
if (command === '/help') {
|
||||
@@ -352,7 +360,7 @@ export class TelegramAlerter {
|
||||
.join('\n');
|
||||
await this.sendMessage(
|
||||
`🤖 *CRUCIX BOT COMMANDS*\n\n${helpText}\n\n_Tip: Commands are case-insensitive_`,
|
||||
{ replyToMessageId: msg.message_id }
|
||||
{ chatId: replyChatId, replyToMessageId: msg.message_id }
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -362,7 +370,7 @@ export class TelegramAlerter {
|
||||
this._muteUntil = Date.now() + hours * 60 * 60 * 1000;
|
||||
await this.sendMessage(
|
||||
`🔇 Alerts muted for ${hours}h — until ${new Date(this._muteUntil).toLocaleTimeString()} UTC\nUse /unmute to resume.`,
|
||||
{ replyToMessageId: msg.message_id }
|
||||
{ chatId: replyChatId, replyToMessageId: msg.message_id }
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -371,7 +379,7 @@ export class TelegramAlerter {
|
||||
this._muteUntil = null;
|
||||
await this.sendMessage(
|
||||
`🔔 Alerts resumed. You'll receive the next signal evaluation.`,
|
||||
{ replyToMessageId: msg.message_id }
|
||||
{ chatId: replyChatId, replyToMessageId: msg.message_id }
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -379,7 +387,7 @@ export class TelegramAlerter {
|
||||
if (command === '/alerts') {
|
||||
const recent = this._alertHistory.slice(-10);
|
||||
if (recent.length === 0) {
|
||||
await this.sendMessage('No recent alerts.', { replyToMessageId: msg.message_id });
|
||||
await this.sendMessage('No recent alerts.', { chatId: replyChatId, replyToMessageId: msg.message_id });
|
||||
return;
|
||||
}
|
||||
const lines = recent.map(a =>
|
||||
@@ -387,7 +395,7 @@ export class TelegramAlerter {
|
||||
);
|
||||
await this.sendMessage(
|
||||
`📋 *Recent Alerts (last ${recent.length})*\n\n${lines.join('\n')}`,
|
||||
{ replyToMessageId: msg.message_id }
|
||||
{ chatId: replyChatId, replyToMessageId: msg.message_id }
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -398,19 +406,80 @@ export class TelegramAlerter {
|
||||
try {
|
||||
const response = await handler(args, msg.message_id);
|
||||
if (response) {
|
||||
await this.sendMessage(response, { replyToMessageId: msg.message_id });
|
||||
await this.sendMessage(response, { chatId: replyChatId, replyToMessageId: msg.message_id });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Telegram] Command ${command} error:`, err.message);
|
||||
await this.sendMessage(
|
||||
`❌ Command failed: ${err.message}`,
|
||||
{ replyToMessageId: msg.message_id }
|
||||
{ chatId: replyChatId, replyToMessageId: msg.message_id }
|
||||
);
|
||||
}
|
||||
}
|
||||
// Unknown commands are silently ignored to avoid spamming
|
||||
}
|
||||
|
||||
async _initializeBotCommands() {
|
||||
await this._loadBotIdentity();
|
||||
|
||||
const botCommands = Object.entries(COMMANDS).map(([command, description]) => ({
|
||||
command: command.replace('/', ''),
|
||||
description: description.substring(0, 256),
|
||||
}));
|
||||
|
||||
// Register globally, then force explicit private/group scopes.
|
||||
await this._setMyCommands(botCommands);
|
||||
await this._setMyCommands(botCommands, { type: 'all_private_chats' });
|
||||
await this._setMyCommands(botCommands, { type: 'all_group_chats' });
|
||||
}
|
||||
|
||||
async _loadBotIdentity() {
|
||||
const res = await fetch(`${TELEGRAM_API}/bot${this.botToken}/getMe`, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text().catch(() => '');
|
||||
throw new Error(`getMe failed (${res.status}): ${err.substring(0, 200)}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.ok || !data.result?.username) {
|
||||
throw new Error('getMe returned invalid bot profile');
|
||||
}
|
||||
this._botUsername = String(data.result.username).toLowerCase();
|
||||
}
|
||||
|
||||
async _setMyCommands(commands, scope = null) {
|
||||
const body = { commands };
|
||||
if (scope) body.scope = scope;
|
||||
|
||||
const res = await fetch(`${TELEGRAM_API}/bot${this.botToken}/setMyCommands`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text().catch(() => '');
|
||||
throw new Error(`setMyCommands failed (${res.status}): ${err.substring(0, 200)}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!data.ok) {
|
||||
throw new Error(`setMyCommands rejected: ${JSON.stringify(data).substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_normalizeCommand(rawCommand) {
|
||||
if (!rawCommand.startsWith('/')) return null;
|
||||
|
||||
const atIdx = rawCommand.indexOf('@');
|
||||
if (atIdx === -1) return rawCommand;
|
||||
|
||||
const command = rawCommand.substring(0, atIdx);
|
||||
const mentionedBot = rawCommand.substring(atIdx + 1).toLowerCase();
|
||||
if (!this._botUsername || mentionedBot === this._botUsername) return command;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ─── Semantic Dedup ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user