diff --git a/public/index.html b/public/index.html index f01c1b1..5cd4dae 100644 --- a/public/index.html +++ b/public/index.html @@ -7,6 +7,10 @@ + + + + @@ -50,6 +54,6 @@ - + diff --git a/public/script.ts b/public/script.ts index ad9c88a..e0bdbec 100644 --- a/public/script.ts +++ b/public/script.ts @@ -59,9 +59,7 @@ class QuixoticApp { this.tg.MainButton.hide(); // Debug Telegram user info - console.log('🔧 Telegram WebApp initialized'); - console.log('👤 User ID:', this.tg.initDataUnsafe?.user?.id); - console.log('📋 Full initData:', this.tg.initDataUnsafe); + console.log('🔧 WebApp ready, user:', this.tg.initDataUnsafe?.user?.id); } else { console.log('❌ Telegram WebApp not available'); } @@ -202,6 +200,26 @@ class QuixoticApp { `).join(''); + // Add touch event listeners to prevent sticky states + this.results.querySelectorAll('.tg-list-item').forEach(item => { + const element = item as HTMLElement; + + // Reset visual state on touch end + element.addEventListener('touchend', () => { + setTimeout(() => { + element.blur(); + element.style.background = ''; + element.style.transform = ''; + }, 100); + }); + + // Also handle mouse leave for desktop + element.addEventListener('mouseleave', () => { + element.style.background = ''; + element.style.transform = ''; + }); + }); + this.results.classList.remove('tg-hidden'); this.results.classList.add('tg-list--visible'); } @@ -217,17 +235,21 @@ class QuixoticApp { } public async convertVideo(videoId: string, title: string, url: string): Promise { - console.log('🎵 CONVERT VIDEO CALLED:', { videoId, title, url }); - console.log('🔧 Telegram WebApp available:', !!this.tg); + console.log('🎵 Converting:', title); // Find the clicked element by looking for the one that contains this videoId const videoElement = document.querySelector(`[onclick*="${videoId}"]`) as HTMLElement; if (videoElement) { + // Force blur to reset any active/focus states + videoElement.blur(); + + // Remove any stuck hover/active classes on touch devices + videoElement.classList.remove('tg-list-item--active'); + videoElement.classList.add('tg-list-item--converting'); } try { - console.log('Sending convert request...'); const response = await fetch('/api/convert', { method: 'POST', headers: { @@ -241,18 +263,13 @@ class QuixoticApp { }) }); - console.log('Response status:', response.status); if (!response.ok) { throw new Error(`Conversion failed with status: ${response.status}`); } const data: ConvertResponse = await response.json(); - console.log('Response data:', data); if (data.audioUrl) { - // MP3 conversion successful! - console.log('🎉 MP3 conversion successful:', data.audioUrl); - console.log('🔧 About to send to Telegram, tg available:', !!this.tg); if (this.tg) { // Try WebApp method first (might not work) @@ -261,52 +278,31 @@ class QuixoticApp { audioUrl: data.audioUrl, title: title }; - console.log('📤 Attempting WebApp sendData first:', payload); - try { - this.tg.sendData(JSON.stringify(payload)); - console.log('✅ WebApp sendData called'); - this.showMessage('✓ MP3 готов! Отправляем в чат...', 'success'); - } catch (sendError) { - console.error('❌ WebApp sendData failed:', sendError); - } - - // Always use direct API as primary method now const userId = this.tg?.initDataUnsafe?.user?.id; - console.log('👤 Current user ID for sending:', userId); - if (!userId) { - console.error('❌ No user ID available from Telegram WebApp'); this.showMessage('❌ Ошибка: не удается определить пользователя', 'error'); return; } + // Send via direct API try { - console.log('🔄 Sending via direct API...'); - const requestData = { - userId: userId, - audioUrl: data.audioUrl, - title: title - }; - console.log('📦 Request data:', requestData); - const directResponse = await fetch('/api/telegram-send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestData) + body: JSON.stringify({ + userId: userId, + audioUrl: data.audioUrl, + title: title + }) }); if (directResponse.ok) { - const result = await directResponse.json(); - console.log('✅ Direct API success:', result); this.showMessage('✅ MP3 отправлен в чат!', 'success'); } else { - const error = await directResponse.json(); - console.error('❌ Direct API failed:', error); this.showMessage('❌ Ошибка отправки в Telegram', 'error'); } } catch (directError) { - console.error('❌ Direct API request failed:', directError); this.showMessage('❌ Ошибка соединения с ботом', 'error'); } } else { diff --git a/public/style.css b/public/style.css index ef05010..5472c7a 100644 --- a/public/style.css +++ b/public/style.css @@ -254,17 +254,30 @@ body { position: relative; } -.tg-list-item:hover { - background: var(--tg-color-secondary-bg); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +/* Hover effects for desktop */ +@media (hover: hover) and (pointer: fine) { + .tg-list-item:hover { + background: var(--tg-color-secondary-bg); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } } +/* Touch feedback for mobile - brief highlight only */ .tg-list-item:active { transform: translateY(0); background: var(--tg-color-secondary-bg); } +/* Prevent sticky hover states on touch devices */ +@media (hover: none) { + .tg-list-item:hover { + background: var(--tg-color-section-bg); + transform: none; + box-shadow: none; + } +} + .tg-list-item__content { display: flex; align-items: center; diff --git a/src/bot.ts b/src/bot.ts index 88fa871..7dcf02a 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -64,19 +64,11 @@ export class QuixoticBot { private setupHandlers(): void { console.log('🔧 Setting up bot handlers...'); - // Log all incoming messages for debugging + // Handle messages 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 } @@ -177,31 +169,21 @@ export class QuixoticBot { // 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) + // Handle callback queries 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 { - console.log('Callback query data is not JSON, ignoring'); + // Not JSON, ignore } } - - // Answer callback query to remove loading state await this.bot.answerCallbackQuery(query.id); }); @@ -259,51 +241,6 @@ export class QuixoticBot { } }); - // 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'); } @@ -319,144 +256,64 @@ export class QuixoticBot { 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}`); + console.log(`📤 Sending: ${title} to chat ${chatId}`); - // 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 sending as audio try { - console.log('🚀 Attempting sendAudio...'); - const audioResult = await this.bot.sendAudio(chatId, audioUrl, { + await this.bot.sendAudio(chatId, audioUrl, { title: title, performer: 'SoundCloud', - caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`, - parse_mode: undefined, - protect_content: false + caption: `🎵 ${title}`, + parse_mode: undefined }); - - 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 { - console.log('Could not delete status message (not critical)'); - } - + console.log(`✅ Audio sent: ${title}`); 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 + // Fallback: try as document try { - console.log('🚀 Attempting sendDocument fallback...'); - const docResult = await this.bot.sendDocument(chatId, audioUrl, { - caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`, + await this.bot.sendDocument(chatId, audioUrl, { + caption: `🎵 ${title}`, 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 { - console.log('Could not delete status message (not critical)'); - } - + console.log(`✅ Document sent: ${title}`); 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); + console.error('❌ Send failed:', error.message); - // Send fallback message with direct link + // Send fallback with 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 - } + `❌ Не удалось отправить файл.\n🎵 ${title}\n🔗 ${audioUrl}` ); - } 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); - } + } catch { + // Silent fail } } } 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, '⏳ Получил запрос, отправляю аудио...'); - + console.log(`🎵 WebApp request: ${data.title}`); 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}`); + console.error('❌ WebApp data parse error:', parseError.message); } } diff --git a/src/server.ts b/src/server.ts index f6cf177..38cc199 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,6 +19,17 @@ const soundcloud = new SoundCloudService(); // Middleware app.use(express.json()); + +// Cache-busting middleware for iOS Safari +app.use('/dist/*.js', (req: Request, res: Response, next) => { + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + next(); +}); + app.use(express.static('public')); // Ensure downloads directory exists @@ -29,7 +40,21 @@ if (!fs.existsSync(downloadsDir)) { // Routes app.get('/', (req: Request, res: Response) => { - res.sendFile(path.join(__dirname, '../public/index.html')); + // Read and modify index.html to add timestamp for iOS cache busting + const indexPath = path.join(__dirname, '../public/index.html'); + let html = fs.readFileSync(indexPath, 'utf8'); + + // Add timestamp to script URL for cache busting + const timestamp = Date.now(); + html = html.replace('dist/script.js?v=2', `dist/script.js?v=${timestamp}`); + + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + + res.send(html); }); // Search videos @@ -189,35 +214,32 @@ app.post('/api/convert', async (req: Request, res: Response) => { } }); -// Direct Telegram API for sending audio when WebApp fails +// Direct Telegram API for sending audio app.post('/api/telegram-send', async (req: Request, res: Response) => { + console.log('🚀 Telegram send request received'); + try { const { userId, audioUrl, title }: { userId?: string; audioUrl?: string; title?: string } = req.body; - console.log('🔄 Direct Telegram send request:', { userId, audioUrl, title }); + console.log(`📤 Sending to user ${userId}: ${title}`); if (!userId || !audioUrl || !title) { return res.status(400).json({ error: 'Missing required fields' }); } - // Get bot instance const botInstance = (global as any).quixoticBot; if (!botInstance) { - console.log('❌ No bot instance available'); + console.log('❌ Bot not available'); return res.status(500).json({ error: 'Bot not available' }); } - console.log('🤖 Using bot instance for direct send'); - - // Use userId as chatId for private chats (this is how Telegram works) const chatId = parseInt(userId); - console.log(`📤 Sending audio to chat ${chatId}`); - await botInstance.sendAudioFile(chatId, audioUrl, title); - console.log('✅ Audio sent successfully via direct API'); + console.log('✅ Audio sent successfully'); res.json({ success: true, message: 'Audio sent successfully' }); + } catch (error: any) { - console.error('❌ Direct send error:', error.message); + console.error('❌ Send failed:', error.message); res.status(500).json({ error: error.message }); } });