Files
quixotic/src/bot.ts
Andrey Kondratev 9bdf522f59 some visual fixes
2025-08-29 18:11:04 +05:00

341 lines
11 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}