import TelegramBot from 'node-telegram-bot-api'; import { Database } from './database'; interface TelegramUser { id: number; first_name: string; last_name?: string; username?: string; } interface Message { chat: { id: number; }; from?: TelegramUser; web_app?: { data: string; }; } interface InlineQuery { id: string; query: string; } interface WebAppData { action: string; audioUrl: string; title: string; } interface SearchResult { query: string; created_at: string; } export class QuixoticBot { private bot: TelegramBot; private webAppUrl: string; private db: Database; constructor(token: string, webAppUrl: string) { this.bot = new TelegramBot(token, { polling: true }); this.webAppUrl = webAppUrl; this.db = new Database(); this.init(); } private init(): void { console.log('🤖 Telegram bot initialized'); this.setupCommands(); this.setupHandlers(); } private setupCommands(): void { // Set bot commands this.bot.setMyCommands([ { command: 'start', description: 'Запустить приложение' }, { command: 'help', description: 'Помощь' }, { command: 'history', description: 'История поиска' } ]); } private setupHandlers(): void { console.log('🔧 Setting up bot handlers...'); // Handle messages this.bot.on('message', (msg: any) => { // Handle web app data in regular message event if (msg.web_app?.data) { this.handleWebAppData(msg); return; // Important: don't process as regular message } }); // Start command this.bot.onText(/\/start/, async (msg: Message) => { const chatId = msg.chat.id; const user = msg.from; try { // Add user to database if (user) { await this.db.addUser(user); } const keyboard = { inline_keyboard: [[ { text: '🎵 Открыть Quixotic', web_app: { url: this.webAppUrl } } ]] }; await this.bot.sendMessage(chatId, '🎵 Добро пожаловать в Quixotic!\n\n' + 'Найди любую песню на SoundCloud и получи MP3 файл прямо в чат.\n\n' + 'Нажми кнопку ниже, чтобы начать поиск:', { reply_markup: keyboard } ); } catch (error) { console.error('Start command error:', error); await this.bot.sendMessage(chatId, '❌ Произошла ошибка. Попробуйте позже.'); } }); // Help command this.bot.onText(/\/help/, async (msg: Message) => { const chatId = msg.chat.id; const helpText = `🎵 *Quixotic - SoundCloud to MP3* *Как пользоваться:* 1️⃣ Нажми кнопку "Открыть Quixotic" 2️⃣ Введи название песни в поисковую строку 3️⃣ Выбери нужный трек из списка 4️⃣ Получи MP3 файл в чат! *Команды:* /start - Запустить приложение /help - Эта справка /history - История поиска *Возможности:* ✅ Поиск по SoundCloud ✅ Высокое качество MP3 (192kbps) ✅ Быстрая конвертация ✅ История поиска`; await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' }); }); // History command this.bot.onText(/\/history/, async (msg: Message) => { const chatId = msg.chat.id; const userId = msg.from?.id; if (!userId) return; try { const user = await this.db.getUserByTelegramId(userId); if (!user) { await this.bot.sendMessage(chatId, 'История поиска пуста.'); return; } // Get recent search history const history = await this.getSearchHistory(user.id); if (history.length === 0) { await this.bot.sendMessage(chatId, 'История поиска пуста.'); return; } let historyText = '📋 *Последние поисковые запросы:*\n\n'; history.forEach((item, index) => { const date = new Date(item.created_at).toLocaleDateString('ru-RU'); historyText += `${index + 1}. ${item.query} _(${date})_\n`; }); await this.bot.sendMessage(chatId, historyText, { parse_mode: 'Markdown' }); } catch (error) { console.error('History command error:', error); await this.bot.sendMessage(chatId, '❌ Ошибка получения истории.'); } }); // Handle web app data - primary event handler this.bot.on('web_app_data', async (msg: Message) => { this.handleWebAppData(msg); }); // Handle callback queries this.bot.on('callback_query', async (query: any) => { if (query.data) { try { const data = JSON.parse(query.data); if (data.action === 'send_audio') { await this.sendAudioFileInternal(query.message.chat.id, data.audioUrl, data.title); } } catch { // Not JSON, ignore } } await this.bot.answerCallbackQuery(query.id); }); // Handle inline queries for search this.bot.on('inline_query', async (query: InlineQuery) => { const queryId = query.id; const searchQuery = query.query; if (!searchQuery || searchQuery.length < 3) { await this.bot.answerInlineQuery(queryId, []); return; } try { const { SoundCloudService } = require('./soundcloud'); const soundcloud = new SoundCloudService(); const videos = await soundcloud.searchTracks(searchQuery, 5); const results = videos.map((video: any, index: number) => ({ type: 'article', id: `${index}`, title: video.title, description: `${video.channel} • ${this.formatDuration(video.duration)}`, thumb_url: video.thumbnail, input_message_content: { message_text: `🎵 ${video.title}\n🔗 ${video.url}` } })); await this.bot.answerInlineQuery(queryId, results, { cache_time: 300, is_personal: true }); } catch (error) { console.error('Inline query error:', error); await this.bot.answerInlineQuery(queryId, []); } }); // Error handler with detailed logging this.bot.on('error', (error: any) => { console.error('🚨 Telegram bot error:', error.message || error); console.error('Error code:', error.code); console.error('Full error:', error); }); // Handle polling errors specifically this.bot.on('polling_error', (error: any) => { console.error('🚨 Telegram polling error:', error.message || error); console.error('Error code:', error.code); // Don't crash on polling errors, just log them if (error.code === 'ETELEGRAM') { console.warn('⚠️ Telegram API error - continuing operation'); } }); console.log('✅ Bot handlers setup complete'); } private async getSearchHistory(userId: number): Promise { return this.db.getSearchHistory(userId); } // Public method for external API calls public async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise { return this.sendAudioFileInternal(chatId, audioUrl, title); } private async sendAudioFileInternal(chatId: number, audioUrl: string, title: string): Promise { try { console.log(`📤 Sending: ${title} to chat ${chatId}`); // Try sending as audio try { await this.bot.sendAudio(chatId, audioUrl, { title: title, performer: 'SoundCloud', caption: `🎵 ${title}`, parse_mode: undefined }); console.log(`✅ Audio sent: ${title}`); return; } catch { // Fallback: try as document try { await this.bot.sendDocument(chatId, audioUrl, { caption: `🎵 ${title}`, parse_mode: undefined }); console.log(`✅ Document sent: ${title}`); return; } catch (documentError: any) { throw documentError; } } } catch (error: any) { console.error('❌ Send failed:', error.message); // Send fallback with link try { await this.bot.sendMessage(chatId, `❌ Не удалось отправить файл.\n🎵 ${title}\n🔗 ${audioUrl}` ); } catch { // Silent fail } } } private async handleWebAppData(msg: Message): Promise { const chatId = msg.chat.id; if (!msg.web_app?.data) { return; } try { const data: WebAppData = JSON.parse(msg.web_app.data); if (data.action === 'send_audio') { console.log(`🎵 WebApp request: ${data.title}`); await this.sendAudioFileInternal(chatId, data.audioUrl, data.title); } } catch (parseError: any) { console.error('❌ WebApp data parse error:', parseError.message); } } private formatDuration(seconds: number): string { if (!seconds) return ''; const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } } // Initialize bot if this file is run directly if (require.main === module) { const token = process.env.TELEGRAM_BOT_TOKEN; const webAppUrl = process.env.WEB_APP_URL || 'https://your-domain.com'; if (!token) { console.error('❌ TELEGRAM_BOT_TOKEN environment variable is required'); process.exit(1); } new QuixoticBot(token, webAppUrl); }