Initial commit: Complete Quixotic Telegram MiniApp implementation

- Set up Express.js server with YouTube search and MP3 conversion API
- Created Telegram Web App frontend with responsive design
- Implemented SQLite database for user management and history
- Added Telegram Bot integration with commands and Web App support
- Configured FFmpeg-based audio conversion pipeline
- Added comprehensive documentation and deployment guides

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrey Kondratev
2025-08-25 10:37:07 +05:00
commit 2bfb456cf3
9 changed files with 968 additions and 0 deletions

223
src/bot.js Normal file
View File

@@ -0,0 +1,223 @@
const TelegramBot = require('node-telegram-bot-api');
const Database = require('./database');
class QuixoticBot {
constructor(token, webAppUrl) {
this.bot = new TelegramBot(token, { polling: true });
this.webAppUrl = webAppUrl;
this.db = new Database();
this.init();
}
init() {
console.log('> Telegram bot initialized');
this.setupCommands();
this.setupHandlers();
}
setupCommands() {
// Set bot commands
this.bot.setMyCommands([
{ command: 'start', description: '0?CAB8BL ?@8;>65=85' },
{ command: 'help', description: '><>IL' },
{ command: 'history', description: 'AB>@8O ?>8A:0' }
]);
}
setupHandlers() {
// Start command
this.bot.onText(/\/start/, async (msg) => {
const chatId = msg.chat.id;
const user = msg.from;
try {
// Add user to database
await this.db.addUser(user);
const keyboard = {
inline_keyboard: [[
{
text: '<µ B:@KBL Quixotic',
web_app: { url: this.webAppUrl }
}
]]
};
await this.bot.sendMessage(chatId,
'<µ >1@> ?>60;>20BL 2 Quixotic!\n\n' +
'0948 ;N1CN ?5A=N =0 YouTube 8 ?>;CG8 MP3 D09; ?@O<> 2 G0B.\n\n' +
'06<8 :=>?:C =865, GB>1K =0G0BL ?>8A::',
{ reply_markup: keyboard }
);
} catch (error) {
console.error('Start command error:', error);
await this.bot.sendMessage(chatId, '@>87>H;0 >H81:0. >?@>1C9B5 ?>765.');
}
});
// Help command
this.bot.onText(/\/help/, async (msg) => {
const chatId = msg.chat.id;
const helpText = `<µ *Quixotic - YouTube to MP3*
*0: ?>;L7>20BLAO:*
1ã 06<8 :=>?:C "B:@KBL Quixotic"
2ã 2548 =0720=85 ?5A=8 2 ?>8A:>2CN AB@>:C
3ã K15@8 =C6=K9 B@5: 87 A?8A:0
4ã >;CG8 MP3 D09; 2 G0B!
*><0=4K:*
/start - 0?CAB8BL ?@8;>65=85
/help - -B0 A?@02:0
/history - AB>@8O ?>8A:0
*>7<>6=>AB8:*
" >8A: ?> YouTube
" KA>:>5 :0G5AB2> MP3 (128kbps)
" KAB@0O :>=25@B0F8O
" AB>@8O ?>8A:0`;
await this.bot.sendMessage(chatId, helpText, { parse_mode: 'Markdown' });
});
// History command
this.bot.onText(/\/history/, async (msg) => {
const chatId = msg.chat.id;
const userId = msg.from.id;
try {
const user = await this.db.getUserByTelegramId(userId);
if (!user) {
await this.bot.sendMessage(chatId, 'AB>@8O ?>8A:0 ?CAB0.');
return;
}
// Get recent search history
const history = await this.db.db.all(
`SELECT query, created_at FROM search_history
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 10`,
[user.id]
);
if (history.length === 0) {
await this.bot.sendMessage(chatId, 'AB>@8O ?>8A:0 ?CAB0.');
return;
}
let historyText = '=È *>A;54=85 ?>8A:>2K5 70?@>AK:*\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, 'H81:0 ?>;CG5=8O 8AB>@88.');
}
});
// Handle web app data
this.bot.on('web_app_data', async (msg) => {
const chatId = msg.chat.id;
const data = JSON.parse(msg.web_app.data);
try {
if (data.action === 'send_audio') {
await this.sendAudioFile(chatId, data.audioUrl, data.title);
}
} catch (error) {
console.error('Web app data error:', error);
await this.bot.sendMessage(chatId, 'H81:0 >1@01>B:8 40==KE.');
}
});
// Handle inline queries for search
this.bot.on('inline_query', async (query) => {
const queryId = query.id;
const searchQuery = query.query;
if (!searchQuery || searchQuery.length < 3) {
await this.bot.answerInlineQuery(queryId, []);
return;
}
try {
const YouTubeService = require('./youtube');
const youtube = new YouTubeService();
const videos = await youtube.searchVideos(searchQuery, 5);
const results = videos.map((video, index) => ({
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
this.bot.on('error', (error) => {
console.error('Telegram bot error:', error);
});
console.log(' Bot handlers setup complete');
}
async sendAudioFile(chatId, audioUrl, title) {
try {
await this.bot.sendMessage(chatId, >43>B02;820N MP3 D09;...');
// Send audio file
await this.bot.sendAudio(chatId, audioUrl, {
title: title,
performer: 'Quixotic',
caption: `${title}`
});
} catch (error) {
console.error('Send audio error:', error);
await this.bot.sendMessage(chatId,
'L 5 C40;>AL >B?@028BL 0C48>D09;. >?@>1C9B5 5I5 @07.\n\n' +
`@O<0O AAK;:0: ${audioUrl}`
);
}
}
formatDuration(seconds) {
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('L TELEGRAM_BOT_TOKEN environment variable is required');
process.exit(1);
}
new QuixoticBot(token, webAppUrl);
}
module.exports = QuixoticBot;