Files
quixotic/src/bot.ts
Andrey Kondratev 7cf833af6f localhost fix
2025-08-29 17:33:13 +05:00

484 lines
18 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...');
// Log all incoming messages for debugging
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
}
});
// 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) => {
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)
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 (e) {
console.log('Callback query data is not JSON, ignoring');
}
}
// Answer callback query to remove loading state
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');
}
});
// 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');
}
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(`🎵 Starting sendAudioFile to chat ${chatId}`);
console.log(`🔗 Audio URL: ${audioUrl}`);
console.log(`📝 Title: ${title}`);
// 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 {
console.log('🚀 Attempting sendAudio...');
const audioResult = await this.bot.sendAudio(chatId, audioUrl, {
title: title,
performer: 'SoundCloud',
caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`,
parse_mode: undefined,
protect_content: false
});
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 (delError) {
console.log('Could not delete status message (not critical)');
}
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
try {
console.log('🚀 Attempting sendDocument fallback...');
const docResult = await this.bot.sendDocument(chatId, audioUrl, {
caption: `🎵 ${title}\n\n🤖 Загружено через Quixotic`,
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 (delError) {
console.log('Could not delete status message (not critical)');
}
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);
// Send fallback message with direct 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
}
);
} 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);
}
}
}
}
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, '⏳ Получил запрос, отправляю аудио...');
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}`);
}
}
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);
}