This commit is contained in:
Andrey Kondratev
2025-11-10 13:56:19 +05:00
parent 6db48b16a7
commit 82a9596370
13 changed files with 1086 additions and 152 deletions

View File

@@ -1,5 +1,6 @@
import TelegramBot from 'node-telegram-bot-api';
import { Database } from './database';
import { logger } from './logger';
interface TelegramUser {
id: number;
@@ -49,7 +50,7 @@ export class QuixoticBot {
const useWebhook = process.env.NODE_ENV === 'production' && process.env.WEBHOOK_URL;
if (useWebhook) {
console.log('🌐 Using webhook mode for production');
logger.telegram('Using webhook mode for production');
this.bot = new TelegramBot(token, {
webHook: {
port: 8443,
@@ -57,7 +58,7 @@ export class QuixoticBot {
}
});
} else {
console.log('🔄 Using polling mode for development');
logger.telegram('Using polling mode for development');
this.bot = new TelegramBot(token, { polling: true });
}
@@ -67,7 +68,7 @@ export class QuixoticBot {
}
private init(): void {
console.log('🤖 Telegram bot initialized');
logger.telegram('Bot initialized');
this.setupCommands();
this.setupHandlers();
}
@@ -82,7 +83,7 @@ export class QuixoticBot {
}
private setupHandlers(): void {
console.log('🔧 Setting up bot handlers...');
logger.telegram('Setting up bot handlers...');
// Handle messages
this.bot.on('message', (msg: any) => {
@@ -108,21 +109,21 @@ export class QuixoticBot {
const keyboard = {
inline_keyboard: [[
{
text: '🎵 Открыть Quixotic',
text: 'Открыть Quixotic',
web_app: { url: this.webAppUrl }
}
]]
};
await this.bot.sendMessage(chatId,
'🎵 Добро пожаловать в Quixotic!\n\n' +
'Добро пожаловать в Quixotic!\n\n' +
'Найди любую песню на SoundCloud и получи MP3 файл прямо в чат.\n\n' +
'Нажми кнопку ниже, чтобы начать поиск:',
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Start command error:', error);
await this.bot.sendMessage(chatId, 'Произошла ошибка. Попробуйте позже.');
logger.error('Start command error:', error);
await this.bot.sendMessage(chatId, 'Произошла ошибка. Попробуйте позже.');
}
});
@@ -130,13 +131,13 @@ export class QuixoticBot {
this.bot.onText(/\/help/, async (msg: Message) => {
const chatId = msg.chat.id;
const helpText = `🎵 *Quixotic - SoundCloud to MP3*
const helpText = `*Quixotic - SoundCloud to MP3*
*Как пользоваться:*
1️⃣ Нажми кнопку "Открыть Quixotic"
2️⃣ Введи название песни в поисковую строку
3️⃣ Выбери нужный трек из списка
4️⃣ Получи MP3 файл в чат!
1. Нажми кнопку "Открыть Quixotic"
2. Введи название песни в поисковую строку
3. Выбери нужный трек из списка
4. Получи MP3 файл в чат!
*Команды:*
/start - Запустить приложение
@@ -144,10 +145,10 @@ export class QuixoticBot {
/history - История поиска
*Возможности:*
Поиск по SoundCloud
Высокое качество MP3 (192kbps)
Быстрая конвертация
История поиска`;
- Поиск по SoundCloud
- Высокое качество MP3 (192kbps)
- Быстрая конвертация
- История поиска`;
await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' });
});
@@ -174,7 +175,7 @@ export class QuixoticBot {
return;
}
let historyText = '📋 *Последние поисковые запросы:*\n\n';
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`;
@@ -182,8 +183,8 @@ export class QuixoticBot {
await this.bot.sendMessage(chatId, historyText, { parse_mode: 'Markdown' });
} catch (error) {
console.error('History command error:', error);
await this.bot.sendMessage(chatId, 'Ошибка получения истории.');
logger.error('History command error:', error);
await this.bot.sendMessage(chatId, 'Ошибка получения истории.');
}
});
@@ -226,10 +227,10 @@ export class QuixoticBot {
type: 'article',
id: `${index}`,
title: video.title,
description: `${video.channel} ${this.formatDuration(video.duration)}`,
description: `${video.channel} - ${this.formatDuration(video.duration)}`,
thumb_url: video.thumbnail,
input_message_content: {
message_text: `🎵 ${video.title}\n🔗 ${video.url}`
message_text: `${video.title}\n${video.url}`
}
}));
@@ -238,31 +239,31 @@ export class QuixoticBot {
is_personal: true
});
} catch (error) {
console.error('Inline query error:', error);
logger.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);
logger.error('Telegram bot error:', error.message || error);
logger.error('Error code:', error.code);
logger.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);
logger.error('Telegram polling error:', error.message || error);
logger.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');
logger.warn('Telegram API error - continuing operation');
}
});
console.log('Bot handlers setup complete');
logger.telegram('Bot handlers setup complete');
}
private async getSearchHistory(userId: number): Promise<SearchResult[]> {
@@ -276,8 +277,9 @@ export class QuixoticBot {
private async sendAudioFileInternal(chatId: number, audioUrlOrPath: string, title: string, performer?: string, thumbnail?: string): Promise<void> {
try {
console.log(`📤 Sending: ${title} to chat ${chatId}`);
console.log(`📂 File source: ${audioUrlOrPath}`);
logger.telegram('Sending audio', `${title} to chat ${chatId}`);
logger.debug(`File source: ${audioUrlOrPath}`);
logger.debug(`Thumbnail: ${thumbnail}`);
// Check if it's a URL or local file path
const isUrl = audioUrlOrPath.startsWith('http');
@@ -288,82 +290,132 @@ export class QuixoticBot {
const urlParts = audioUrlOrPath.split('/');
const filename = urlParts[urlParts.length - 1];
filePath = require('path').join(process.cwd(), 'downloads', filename);
console.log(`📂 Converted URL to local path: ${filePath}`);
logger.debug(`Converted URL to local path: ${filePath}`);
}
const fs = require('fs');
const path = require('path');
const https = require('https');
// Check if file exists
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
logger.error(`File not found: ${filePath}`);
throw new Error('File not found: ' + filePath);
}
// Get file stats for debugging
const stats = fs.statSync(filePath);
console.log(`📊 File size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
logger.debug(`File size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
// Generate custom filename for display
const safeTitle = (title || 'audio').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 30);
const safePerformer = (performer || '').replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 20);
const customFilename = safePerformer ? `${safePerformer} - ${safeTitle}.mp3` : `${safeTitle}.mp3`;
console.log(`📝 Sending as: ${customFilename}`);
logger.debug(`Sending as: ${customFilename}`);
// Download thumbnail if provided
let thumbnailPath: string | undefined;
if (thumbnail && thumbnail.startsWith('http')) {
try {
logger.debug('Downloading thumbnail...');
const thumbnailFilename = `thumb_${Date.now()}.jpg`;
thumbnailPath = path.join(process.cwd(), 'downloads', thumbnailFilename);
await new Promise<void>((resolve, reject) => {
const file = fs.createWriteStream(thumbnailPath);
https.get(thumbnail, (response: any) => {
response.pipe(file);
file.on('finish', () => {
file.close();
resolve();
});
}).on('error', (err: any) => {
fs.unlink(thumbnailPath, () => {});
reject(err);
});
});
logger.success(`Thumbnail downloaded: ${thumbnailPath}`);
} catch (thumbError: any) {
logger.warn('Failed to download thumbnail:', thumbError.message);
thumbnailPath = undefined;
}
}
// Send file using stream (better for large files)
const fileStream = fs.createReadStream(filePath);
// Try sending as audio with metadata
try {
await this.bot.sendAudio(chatId, fileStream, {
const options: any = {
title: title,
performer: performer || 'Unknown Artist',
caption: undefined,
thumbnail: undefined, // Thumbnail requires special handling
parse_mode: undefined
}, {
};
// Add thumbnail if downloaded
if (thumbnailPath) {
options.thumbnail = fs.createReadStream(thumbnailPath);
}
await this.bot.sendAudio(chatId, fileStream, options, {
filename: customFilename,
contentType: 'audio/mpeg'
});
console.log(`✅ Audio sent successfully: ${title}`);
logger.success(`Audio sent successfully: ${title}`);
// Clean up thumbnail file
if (thumbnailPath) {
fs.unlink(thumbnailPath, (err: any) => {
if (err) logger.error('Failed to delete thumbnail:', err);
});
}
return;
} catch (error: any) {
console.error('Audio send failed:', error.message);
console.error('Error code:', error.code);
logger.error('Audio send failed:', error.message);
logger.error('Error code:', error.code);
// Clean up thumbnail file on error
if (thumbnailPath) {
fs.unlink(thumbnailPath, () => {});
}
// Fallback: try as document
try {
console.log('🔄 Retrying as document...');
logger.info('Retrying as document...');
const docStream = fs.createReadStream(filePath);
await this.bot.sendDocument(chatId, docStream, {
caption: `🎵 ${title}\n👤 ${performer || 'Unknown Artist'}`,
caption: `${title}\n${performer || 'Unknown Artist'}`,
parse_mode: undefined
}, {
filename: customFilename,
contentType: 'audio/mpeg'
});
console.log(`Document sent successfully: ${title}`);
logger.success(`Document sent successfully: ${title}`);
return;
} catch (documentError: any) {
console.error('Document send also failed:', documentError.message);
logger.error('Document send also failed:', documentError.message);
throw documentError;
}
}
} catch (error: any) {
console.error('Send failed completely:', error.message);
console.error('Full error:', error);
logger.error('Send failed completely:', error.message);
logger.error('Full error:', error);
// Send error message to user
try {
await this.bot.sendMessage(chatId,
`Не удалось отправить файл.\n🎵 ${title}\n\опробуйте другой трек.`
`Не удалось отправить файл.\n${title}\n\опробуйте другой трек.`
);
} catch {
console.error('Could not even send error message');
logger.error('Could not even send error message');
}
// Re-throw to trigger unhandled rejection handler
@@ -382,11 +434,11 @@ export class QuixoticBot {
const data: WebAppData = JSON.parse(msg.web_app.data);
if (data.action === 'send_audio') {
console.log(`🎵 WebApp request: ${data.title}`);
logger.telegram('WebApp request', data.title);
await this.sendAudioFileInternal(chatId, data.audioUrl, data.title);
}
} catch (parseError: any) {
console.error('WebApp data parse error:', parseError.message);
logger.error('WebApp data parse error:', parseError.message);
}
}
@@ -405,7 +457,7 @@ if (require.main === module) {
const webAppUrl = process.env.WEB_APP_URL || 'https://your-domain.com';
if (!token) {
console.error('TELEGRAM_BOT_TOKEN environment variable is required');
logger.error('TELEGRAM_BOT_TOKEN environment variable is required');
process.exit(1);
}