mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-04 19:02:33 -06:00
142 lines
3.7 KiB
Go
142 lines
3.7 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{}
|
||
}
|
||
|
||
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("🧾 <b>Результат (%d поз.):</b>\n\n", len(items)))
|
||
|
||
matchedCount := 0
|
||
for _, item := range items {
|
||
if item.IsMatched {
|
||
matchedCount++
|
||
sb.WriteString(fmt.Sprintf("✅ %s\n └ <code>%s</code> x %s = %s\n",
|
||
item.RawName, item.Amount, item.Price, item.Sum))
|
||
} else {
|
||
sb.WriteString(fmt.Sprintf("❓ <b>%s</b>\n └ Нет привязки!\n", item.RawName))
|
||
}
|
||
}
|
||
|
||
sb.WriteString(fmt.Sprintf("\nРаспознано: %d/%d", matchedCount, len(items)))
|
||
|
||
// Тут можно добавить кнопки, если что-то не распознано
|
||
// Но для начала просто текст
|
||
return c.Send(sb.String(), tele.ModeHTML)
|
||
}
|