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{} } 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, } 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("⏳ Обрабатываю чек через OCR...") // 2. Отправляем в сервис ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() items, err := bot.ocrService.ProcessReceiptImage(ctx, imgData) if err != nil { logger.Log.Error("OCR processing failed", zap.Error(err)) return c.Send("❌ Ошибка распознавания: " + err.Error()) } // 3. Формируем отчет var sb strings.Builder sb.WriteString(fmt.Sprintf("🧾 Результат (%d поз.):\n\n", len(items))) matchedCount := 0 for _, item := range items { if item.IsMatched { matchedCount++ sb.WriteString(fmt.Sprintf("✅ %s\n └ %s x %s = %s\n", item.RawName, item.Amount, item.Price, item.Sum)) } else { sb.WriteString(fmt.Sprintf("❓ %s\n └ Нет привязки!\n", item.RawName)) } } sb.WriteString(fmt.Sprintf("\nРаспознано: %d/%d", matchedCount, len(items))) // Тут можно добавить кнопки, если что-то не распознано // Но для начала просто текст return c.Send(sb.String(), tele.ModeHTML) }