package telegram import ( "context" "fmt" "io" "net/http" "strings" "time" "go.uber.org/zap" tele "gopkg.in/telebot.v3" "gopkg.in/telebot.v3/middleware" "rmser/config" "rmser/internal/services/ocr" "rmser/pkg/logger" ) type Bot struct { b *tele.Bot ocrService *ocr.Service adminIDs map[int64]struct{} webAppURL string } func NewBot(cfg config.TelegramConfig, ocrService *ocr.Service) (*Bot, error) { pref := tele.Settings{ Token: cfg.Token, Poller: &tele.LongPoller{Timeout: 10 * time.Second}, OnError: func(err error, c tele.Context) { logger.Log.Error("Telegram error", zap.Error(err)) }, } b, err := tele.NewBot(pref) if err != nil { return nil, err } admins := make(map[int64]struct{}) for _, id := range cfg.AdminIDs { admins[id] = struct{}{} } bot := &Bot{ b: b, ocrService: ocrService, adminIDs: admins, webAppURL: cfg.WebAppURL, } // Если в конфиге пусто, ставим заглушку, чтобы не падало, но предупреждаем if bot.webAppURL == "" { logger.Log.Warn("Telegram WebAppURL не задан в конфиге! Кнопки работать не будут.") bot.webAppURL = "http://example.com" } bot.initHandlers() return bot, nil } func (bot *Bot) Start() { logger.Log.Info("Запуск Telegram бота...") bot.b.Start() } func (bot *Bot) Stop() { bot.b.Stop() } // Middleware для проверки прав (только админы) func (bot *Bot) authMiddleware(next tele.HandlerFunc) tele.HandlerFunc { return func(c tele.Context) error { if len(bot.adminIDs) > 0 { if _, ok := bot.adminIDs[c.Sender().ID]; !ok { return c.Send("⛔ У вас нет доступа к этому боту.") } } return next(c) } } func (bot *Bot) initHandlers() { bot.b.Use(middleware.Logger()) bot.b.Use(bot.authMiddleware) bot.b.Handle("/start", func(c tele.Context) error { return c.Send("👋 Привет! Я RMSER Bot.\nОтправь мне фото накладной или чека, и я попробую его распознать.") }) bot.b.Handle(tele.OnPhoto, bot.handlePhoto) } func (bot *Bot) handlePhoto(c tele.Context) error { // 1. Скачиваем фото photo := c.Message().Photo // Берем файл самого высокого качества (последний в массиве, но telebot дает удобный доступ) file, err := bot.b.FileByID(photo.FileID) if err != nil { return c.Send("Ошибка доступа к файлу.") } // Читаем тело файла fileURL := fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", bot.b.Token, file.FilePath) resp, err := http.Get(fileURL) if err != nil { return c.Send("Ошибка скачивания файла.") } defer resp.Body.Close() imgData, err := io.ReadAll(resp.Body) if err != nil { return c.Send("Ошибка чтения файла.") } c.Send("⏳ Обрабатываю чек: создаю черновик и распознаю...") // 2. Отправляем в сервис (добавили ID чата) ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) // Чуть увеличим таймаут defer cancel() draft, err := bot.ocrService.ProcessReceiptImage(ctx, c.Chat().ID, imgData) if err != nil { logger.Log.Error("OCR processing failed", zap.Error(err)) return c.Send("❌ Ошибка обработки: " + err.Error()) } // 3. Анализ результатов для сообщения matchedCount := 0 for _, item := range draft.Items { if item.IsMatched { matchedCount++ } } // Формируем URL. Для Mini App это должен быть https URL вашего фронтенда. // Фронтенд должен уметь роутить /invoice/:id baseURL := strings.TrimRight(bot.webAppURL, "/") fullURL := fmt.Sprintf("%s/invoice/%s", baseURL, draft.ID.String()) // Формируем текст сообщения var msgText string if matchedCount == len(draft.Items) { msgText = fmt.Sprintf("✅ Успех! Все позиции (%d) распознаны.\n\nПереходите к созданию накладной.", len(draft.Items)) } else { msgText = fmt.Sprintf("⚠️ Внимание! Распознано %d из %d позиций.\n\nНекоторые товары требуют ручного сопоставления. Нажмите кнопку ниже, чтобы исправить.", matchedCount, len(draft.Items)) } menu := &tele.ReplyMarkup{} // Используем WebApp, а не URL btnOpen := menu.WebApp("📝 Открыть накладную", &tele.WebApp{ URL: fullURL, }) menu.Inline( menu.Row(btnOpen), ) return c.Send(msgText, menu, tele.ModeHTML) }