341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
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<SearchResult[]> {
|
||
return this.db.getSearchHistory(userId);
|
||
}
|
||
|
||
// Public method for external API calls
|
||
public async sendAudioFile(chatId: number, audioUrl: string, title: string): Promise<void> {
|
||
return this.sendAudioFileInternal(chatId, audioUrl, title);
|
||
}
|
||
|
||
private async sendAudioFileInternal(chatId: number, audioUrl: string, title: string): Promise<void> {
|
||
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<void> {
|
||
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);
|
||
}
|