auction-scrapper/app/services/telegram_service.ts
Vakula Uladimir 12f005e335 init
2025-10-17 11:27:52 +03:00

405 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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 { 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<void> {
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 <id> - удалить ключевое слово\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<void> {
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<void> {
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 <id>`)
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<void> {
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 <id>\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<void> {
const helpMessage =
'🤖 Помощь по использованию бота\n\n' +
'Доступные команды:\n\n' +
'/start - Регистрация в системе\n' +
'/addkeyword <слово> - Добавить ключевое слово для отслеживания\n' +
'/listkeywords - Список ваших ключевых слов\n' +
'/deletekeyword <id> - Удалить ключевое слово\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<boolean> {
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<void> {
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<void> {
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
}
}