safari fix

This commit is contained in:
Andrey Kondratev
2025-08-29 18:05:35 +05:00
parent a08fd5b495
commit 8e3c927cf1
5 changed files with 110 additions and 218 deletions

View File

@@ -7,6 +7,10 @@
<meta name="theme-color" content="#007AFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<link rel="stylesheet" href="style.css">
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
@@ -50,6 +54,6 @@
</div>
</div>
<script src="dist/script.js"></script>
<script src="dist/script.js?v=2"></script>
</body>
</html>

View File

@@ -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 {
</div>
`).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<void> {
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 {

View File

@@ -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;

View File

@@ -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<void> {
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<void> {
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);
}
}

View File

@@ -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 });
}
});