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)
}