import { Bot, Context } from 'grammy' import env from '#start/env' import logger from '@adonisjs/core/services/logger' import User from '#models/user' import Keyword from '#models/keyword' /** * TelegramService - Handles Telegram bot operations * * Singleton service for managing Grammy bot instance and commands. * Provides keyword management and notification delivery. */ export class TelegramService { private static instance: TelegramService | null = null private bot: Bot private isRunning: boolean = false private constructor() { const token = env.get('TELEGRAM_BOT_TOKEN') this.bot = new Bot(token) this.setupCommands() } /** * Get singleton instance of TelegramService */ static getInstance(): TelegramService { if (!TelegramService.instance) { TelegramService.instance = new TelegramService() } return TelegramService.instance } /** * Setup bot commands and handlers */ private setupCommands(): void { // /start - Register user this.bot.command('start', async (ctx) => { try { await this.handleStart(ctx) } catch (error) { logger.error('Error handling /start command', { error: error instanceof Error ? error.message : String(error), chatId: ctx.chat?.id, }) await ctx.reply('Произошла ошибка при регистрации. Попробуйте позже.') } }) // /addkeyword - Add keyword this.bot.command('addkeyword', async (ctx) => { try { await this.handleAddKeyword(ctx) } catch (error) { logger.error('Error handling /addkeyword command', { error: error instanceof Error ? error.message : String(error), chatId: ctx.chat?.id, }) await ctx.reply('Произошла ошибка при добавлении ключевого слова. Попробуйте позже.') } }) // /listkeywords - List keywords this.bot.command('listkeywords', async (ctx) => { try { await this.handleListKeywords(ctx) } catch (error) { logger.error('Error handling /listkeywords command', { error: error instanceof Error ? error.message : String(error), chatId: ctx.chat?.id, }) await ctx.reply('Произошла ошибка при получении списка ключевых слов. Попробуйте позже.') } }) // /deletekeyword - Delete keyword this.bot.command('deletekeyword', async (ctx) => { try { await this.handleDeleteKeyword(ctx) } catch (error) { logger.error('Error handling /deletekeyword command', { error: error instanceof Error ? error.message : String(error), chatId: ctx.chat?.id, }) await ctx.reply('Произошла ошибка при удалении ключевого слова. Попробуйте позже.') } }) // /help - Show help this.bot.command('help', async (ctx) => { await this.handleHelp(ctx) }) // Error handler this.bot.catch((err) => { logger.error('Grammy bot error', { error: err.error instanceof Error ? err.error.message : String(err.error), ctx: err.ctx, }) }) } /** * Handle /start command - Register user */ private async handleStart(ctx: Context): Promise { if (!ctx.chat?.id) { await ctx.reply('Не удалось определить chat ID.') return } const chatId = String(ctx.chat.id) // Check if user already exists const existingUser = await User.findBy('telegramChatId', chatId) if (existingUser) { await ctx.reply( 'Вы уже зарегистрированы!\n\n' + 'Используйте /addkeyword для добавления ключевых слов.\n' + 'Используйте /help для получения справки.' ) logger.info(`User already registered: ${chatId}`) return } // Create new user const user = await User.create({ email: `telegram_${chatId}@temp.local`, password: Math.random().toString(36).substring(2, 15), telegramChatId: chatId, }) await ctx.reply( 'Добро пожаловать! Вы успешно зарегистрированы.\n\n' + 'Теперь вы можете добавлять ключевые слова для отслеживания аукционов:\n' + '/addkeyword <слово> - добавить ключевое слово\n' + '/listkeywords - список ваших ключевых слов\n' + '/deletekeyword - удалить ключевое слово\n' + '/help - справка' ) logger.info(`New user registered: ${chatId} (user_id: ${user.id})`) } /** * Handle /addkeyword command - Add keyword for user */ private async handleAddKeyword(ctx: Context): Promise { if (!ctx.chat?.id) { await ctx.reply('Не удалось определить chat ID.') return } const chatId = String(ctx.chat.id) const user = await User.findBy('telegramChatId', chatId) if (!user) { await ctx.reply('Вы не зарегистрированы. Используйте /start для регистрации.') return } // Extract keyword from message const messageText = ctx.message?.text || '' const parts = messageText.split(' ') if (parts.length < 2) { await ctx.reply( 'Использование: /addkeyword <слово>\n\n' + 'Пример: /addkeyword строительство' ) return } const keyword = parts.slice(1).join(' ').trim() if (keyword.length === 0) { await ctx.reply('Ключевое слово не может быть пустым.') return } if (keyword.length > 255) { await ctx.reply('Ключевое слово слишком длинное (максимум 255 символов).') return } // Check if keyword already exists for this user const existingKeyword = await Keyword.query() .where('userId', user.id) .where('keyword', keyword) .first() if (existingKeyword) { await ctx.reply(`Ключевое слово "${keyword}" уже добавлено.`) return } // Create keyword const newKeyword = await Keyword.create({ userId: user.id, keyword: keyword, isActive: true, }) await ctx.reply(`Ключевое слово "${keyword}" успешно добавлено (ID: ${newKeyword.id}).`) logger.info(`Keyword added: "${keyword}" for user ${user.id} (chat_id: ${chatId})`) } /** * Handle /listkeywords command - List user's keywords */ private async handleListKeywords(ctx: Context): Promise { if (!ctx.chat?.id) { await ctx.reply('Не удалось определить chat ID.') return } const chatId = String(ctx.chat.id) const user = await User.findBy('telegramChatId', chatId) if (!user) { await ctx.reply('Вы не зарегистрированы. Используйте /start для регистрации.') return } // Fetch user's keywords const keywords = await Keyword.query().where('userId', user.id).where('isActive', true) if (keywords.length === 0) { await ctx.reply( 'У вас нет активных ключевых слов.\n\n' + 'Используйте /addkeyword для добавления ключевых слов.' ) return } // Format keywords list const keywordsList = keywords .map((kw) => `${kw.id}. ${kw.keyword}`) .join('\n') await ctx.reply(`Ваши ключевые слова:\n\n${keywordsList}\n\nДля удаления используйте: /deletekeyword `) logger.info(`Listed ${keywords.length} keyword(s) for user ${user.id} (chat_id: ${chatId})`) } /** * Handle /deletekeyword command - Delete keyword by ID */ private async handleDeleteKeyword(ctx: Context): Promise { if (!ctx.chat?.id) { await ctx.reply('Не удалось определить chat ID.') return } const chatId = String(ctx.chat.id) const user = await User.findBy('telegramChatId', chatId) if (!user) { await ctx.reply('Вы не зарегистрированы. Используйте /start для регистрации.') return } // Extract keyword ID from message const messageText = ctx.message?.text || '' const parts = messageText.split(' ') if (parts.length < 2) { await ctx.reply('Использование: /deletekeyword \n\n' + 'Пример: /deletekeyword 5') return } const keywordId = Number.parseInt(parts[1], 10) if (Number.isNaN(keywordId)) { await ctx.reply('Некорректный ID. Используйте числовое значение.') return } // Find keyword const keyword = await Keyword.query() .where('id', keywordId) .where('userId', user.id) .first() if (!keyword) { await ctx.reply(`Ключевое слово с ID ${keywordId} не найдено.`) return } // Soft delete - set isActive to false keyword.isActive = false await keyword.save() await ctx.reply(`Ключевое слово "${keyword.keyword}" (ID: ${keywordId}) успешно удалено.`) logger.info( `Keyword deleted: "${keyword.keyword}" (ID: ${keywordId}) for user ${user.id} (chat_id: ${chatId})` ) } /** * Handle /help command - Show help message */ private async handleHelp(ctx: Context): Promise { const helpMessage = '🤖 Помощь по использованию бота\n\n' + 'Доступные команды:\n\n' + '/start - Регистрация в системе\n' + '/addkeyword <слово> - Добавить ключевое слово для отслеживания\n' + '/listkeywords - Список ваших ключевых слов\n' + '/deletekeyword - Удалить ключевое слово\n' + '/help - Показать эту справку\n\n' + 'Примеры использования:\n' + '/addkeyword строительство\n' + '/deletekeyword 5' await ctx.reply(helpMessage) } /** * Send notification to user about matched auction */ async sendNotification( chatId: string, auctionTitle: string, auctionUrl: string, keyword: string ): Promise { try { const message = `🔔 Новый аукцион по ключевому слову "${keyword}"\n\n` + `📋 ${auctionTitle}\n\n` + `🔗 ${auctionUrl}` await this.bot.api.sendMessage(chatId, message, { parse_mode: 'HTML', }) logger.info(`Notification sent to chat ${chatId} for keyword "${keyword}"`) return true } catch (error) { logger.error('Failed to send notification', { error: error instanceof Error ? error.message : String(error), chatId, keyword, }) return false } } /** * Start the bot (long polling) */ async start(): Promise { if (this.isRunning) { logger.warn('Bot is already running') return } try { logger.info('Starting Telegram bot...') this.isRunning = true await this.bot.start() logger.info('Telegram bot started successfully') } catch (error) { this.isRunning = false logger.error('Failed to start Telegram bot', { error: error instanceof Error ? error.message : String(error), }) throw error } } /** * Stop the bot gracefully */ async stop(): Promise { if (!this.isRunning) { logger.warn('Bot is not running') return } try { logger.info('Stopping Telegram bot...') await this.bot.stop() this.isRunning = false logger.info('Telegram bot stopped successfully') } catch (error) { logger.error('Failed to stop Telegram bot', { error: error instanceof Error ? error.message : String(error), }) throw error } } /** * Check if bot is running */ getIsRunning(): boolean { return this.isRunning } }