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...'); // Log all incoming messages for debugging this.bot.on('message', (msg: any) => { console.log('📨 Received message:', { type: msg.web_app ? 'web_app_data' : (msg.text ? 'text' : 'other'), from: msg.from?.id, chat: msg.chat?.id, hasWebAppData: !!msg.web_app?.data, webAppDataLength: msg.web_app?.data?.length || 0 }); // Handle web app data in regular message event if (msg.web_app?.data) { console.log('🔍 Web app data found in message:', 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) => { console.log('🔍 Web app data received via web_app_data event:', msg.web_app?.data); this.handleWebAppData(msg); }); // Additional handler for callback queries (sometimes WebApp data comes here) this.bot.on('callback_query', async (query: any) => { console.log('📞 Callback query received:', { id: query.id, data: query.data, from: query.from?.id }); if (query.data) { try { const data = JSON.parse(query.data); if (data.action === 'send_audio') { console.log('🎵 Audio action from callback query'); await this.sendAudioFileInternal(query.message.chat.id, data.audioUrl, data.title); } } catch (e) { console.log('Callback query data is not JSON, ignoring'); } } // Answer callback query to remove loading state 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'); } }); // Add universal event logger for debugging this.bot.on('edited_message', (msg: any) => { console.log('✏️ Edited message received, checking for web app data:', !!msg.web_app?.data); if (msg.web_app?.data) { this.handleWebAppData(msg); } }); this.bot.on('channel_post', (msg: any) => { console.log('📢 Channel post received, checking for web app data:', !!msg.web_app?.data); if (msg.web_app?.data) { this.handleWebAppData(msg); } }); // Log all update types this.bot.on('update', (update: any) => { const updateTypes = Object.keys(update).filter(key => key !== 'update_id'); console.log('🔄 Update received:', { types: updateTypes, update_id: update.update_id }); // Check for web_app_data in any part of the update const checkForWebAppData = (obj: any, path = ''): any => { if (!obj || typeof obj !== 'object') return null; if (obj.web_app?.data) { console.log(`🎯 Found web_app_data at ${path}:`, obj.web_app.data); return obj; } for (const [key, value] of Object.entries(obj)) { const result = checkForWebAppData(value, `${path}.${key}`); if (result) return result; } return null; }; const msgWithData = checkForWebAppData(update); if (msgWithData && msgWithData.chat?.id) { console.log('🚀 Processing web app data found in update'); this.handleWebAppData(msgWithData); } }); 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(`🎵 Starting sendAudioFile to chat ${chatId}`); console.log(`🔗 Audio URL: ${audioUrl}`); console.log(`📝 Title: ${title}`); // Send initial status message const statusMsg = await this.bot.sendMessage(chatId, '⏳ Подготавливаю MP3 файл...'); // Validate audio URL if (!audioUrl.startsWith('http')) { throw new Error(`Invalid audio URL: ${audioUrl}`); } // Try sending as audio with proper error handling try { console.log('🚀 Attempting sendAudio...'); const audioResult = await this.bot.sendAudio(chatId, audioUrl, { title: title, performer: 'SoundCloud', caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`, parse_mode: undefined, protect_content: false }); console.log(`✅ Audio sent successfully! Message ID: ${audioResult.message_id}`); // Delete status message after success try { await this.bot.deleteMessage(chatId, statusMsg.message_id); } catch (delError) { console.log('Could not delete status message (not critical)'); } return; } catch (audioError: any) { console.error('❌ SendAudio failed:', audioError.message); console.error('Audio error details:', audioError); // Update status message await this.bot.editMessageText('📄 Отправляю как документ...', { chat_id: chatId, message_id: statusMsg.message_id }); // Fallback: try sending as document try { console.log('🚀 Attempting sendDocument fallback...'); const docResult = await this.bot.sendDocument(chatId, audioUrl, { caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`, parse_mode: undefined }); console.log(`✅ Document sent successfully! Message ID: ${docResult.message_id}`); // Delete status message after success try { await this.bot.deleteMessage(chatId, statusMsg.message_id); } catch (delError) { console.log('Could not delete status message (not critical)'); } return; } catch (documentError: any) { console.error('❌ SendDocument also failed:', documentError.message); console.error('Document error details:', documentError); throw documentError; } } } catch (error: any) { console.error('💥 Complete send audio failure:', error.message); console.error('Full error object:', error); // Send fallback message with direct link try { await this.bot.sendMessage(chatId, `❌ Не удалось отправить файл автоматически.\n\n` + `🎵 *${title}*\n\n` + `📥 Скачайте напрямую: [Ссылка на MP3](${audioUrl})\n\n` + `_Ошибка: ${error.message}_`, { parse_mode: 'Markdown', disable_web_page_preview: false } ); } catch (msgError: any) { console.error('💥 Failed to send error message:', msgError.message); // Last resort - try without markdown try { await this.bot.sendMessage(chatId, `❌ Ошибка отправки файла.\n🎵 ${title}\n🔗 ${audioUrl}` ); } catch (lastError) { console.error('💥 All fallback methods failed:', lastError); } } } } private async handleWebAppData(msg: Message): Promise { const chatId = msg.chat.id; const userId = msg.from?.id; console.log('🔧 HandleWebAppData called with:', { chatId, userId, hasWebAppData: !!msg.web_app?.data, dataLength: msg.web_app?.data?.length || 0 }); if (!msg.web_app?.data) { console.log('❌ No web app data found in message'); await this.bot.sendMessage(chatId, '❌ Данные не получены. Попробуйте еще раз.'); return; } try { console.log('📝 Raw web app data:', msg.web_app.data); const data: WebAppData = JSON.parse(msg.web_app.data); console.log('✅ Parsed data successfully:', data); if (data.action === 'send_audio') { console.log(`🎵 Processing audio request for user ${userId}, chat ${chatId}: ${data.title}`); console.log(`🔗 Audio URL: ${data.audioUrl}`); // Send immediate confirmation await this.bot.sendMessage(chatId, '⏳ Получил запрос, отправляю аудио...'); await this.sendAudioFileInternal(chatId, data.audioUrl, data.title); } else { console.log('⚠️ Unknown action:', data.action); await this.bot.sendMessage(chatId, `❌ Неизвестное действие: ${data.action}`); } } catch (parseError: any) { console.error('❌ Web app data parse error:', parseError.message); console.error('Raw data that failed to parse:', msg.web_app?.data); await this.bot.sendMessage(chatId, `❌ Ошибка обработки данных: ${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); }