mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
163 lines
4.8 KiB
Go
163 lines
4.8 KiB
Go
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("✅ <b>Успех!</b> Все позиции (%d) распознаны.\n\nПереходите к созданию накладной.", len(draft.Items))
|
||
} else {
|
||
msgText = fmt.Sprintf("⚠️ <b>Внимание!</b> Распознано %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)
|
||
}
|