Restructured project
- backend moved to backend directory - added and initialized frontend with vue - moved infrastructure files to infra directory
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"FamilyHub/src/bot/handlers"
|
||||
"FamilyHub/src/config"
|
||||
"FamilyHub/src/integrations/familyHub"
|
||||
"FamilyHub/src/integrations/ocr"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
api *tgbotapi.BotAPI
|
||||
ocr ocr.OCR
|
||||
router *Router
|
||||
}
|
||||
|
||||
func NewBot(cfg config.Config) (*Bot, error) {
|
||||
api, err := tgbotapi.NewBotAPI(cfg.BotToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create telegram bot api: %w", err)
|
||||
}
|
||||
api.Debug = cfg.DebugMode
|
||||
|
||||
ctx := context.Background()
|
||||
ocrSvc, err := ocr.NewGoogleOCR(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create google ocr client: %w", err)
|
||||
}
|
||||
|
||||
apiHost := strings.TrimSpace(cfg.APIHost)
|
||||
if apiHost == "" {
|
||||
apiHost = "localhost"
|
||||
}
|
||||
apiPort := strings.TrimSpace(cfg.APIPort)
|
||||
if apiPort == "" {
|
||||
apiPort = "8000"
|
||||
}
|
||||
|
||||
receiptAPI, err := familyHub.NewApiClient(cfg)
|
||||
if err != nil {
|
||||
_ = ocrSvc.Close()
|
||||
return nil, fmt.Errorf("create family hub api client: %w", err)
|
||||
}
|
||||
handler := handlers.New(api, ocrSvc, receiptAPI)
|
||||
|
||||
return &Bot{api: api, ocr: ocrSvc, router: NewRouter(handler)}, nil
|
||||
}
|
||||
|
||||
func (bot *Bot) Start(ctx context.Context) error {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 1
|
||||
updates := bot.api.GetUpdatesChan(u)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Telegram bot stopping...")
|
||||
bot.api.StopReceivingUpdates()
|
||||
if bot.ocr != nil {
|
||||
_ = bot.ocr.Close()
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
return nil
|
||||
case update, ok := <-updates:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
bot.router.Handle(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/api/services"
|
||||
"FamilyHub/src/domain"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func (h *Handler) HandleCreateFamily(msg *tgbotapi.Message) {
|
||||
if msg.From == nil {
|
||||
h.reply(msg.Chat.ID, "Не удалось определить пользователя Telegram")
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Chat == nil || msg.Chat.Type != "supergroup" {
|
||||
h.reply(msg.Chat.ID, "Для создания семьи переведи бота в супергруппу и запусти /createFamily там")
|
||||
return
|
||||
}
|
||||
|
||||
h.setFamilyState(msg.From.ID, familyCreationState{AwaitingName: true, ChatID: msg.Chat.ID})
|
||||
h.reply(msg.Chat.ID, "Введи имя семьи одним сообщением")
|
||||
}
|
||||
|
||||
func (h *Handler) handleCreateFamilyName(msg *tgbotapi.Message) {
|
||||
if msg.From == nil || msg.Chat == nil {
|
||||
return
|
||||
}
|
||||
|
||||
familyName := strings.TrimSpace(msg.Text)
|
||||
if familyName == "" {
|
||||
h.reply(msg.Chat.ID, "Имя семьи не может быть пустым. Введи имя еще раз")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
user, err := h.receiptApi.GetUserByTelegramID(ctx, msg.From.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, services.ErrUserNotFound) {
|
||||
h.reply(msg.Chat.ID, "Сначала зарегистрируйся: /register")
|
||||
return
|
||||
}
|
||||
log.Printf("failed to get user by telegram id: %v", err)
|
||||
h.reply(msg.Chat.ID, "Не удалось получить пользователя приложения")
|
||||
return
|
||||
}
|
||||
|
||||
promoteCfg := tgbotapi.PromoteChatMemberConfig{
|
||||
ChatMemberConfig: tgbotapi.ChatMemberConfig{
|
||||
ChatID: msg.Chat.ID,
|
||||
UserID: msg.From.ID,
|
||||
},
|
||||
CanManageChat: true,
|
||||
CanChangeInfo: true,
|
||||
CanDeleteMessages: true,
|
||||
CanManageVoiceChats: true,
|
||||
CanInviteUsers: true,
|
||||
CanRestrictMembers: true,
|
||||
CanPinMessages: true,
|
||||
}
|
||||
if _, err := h.api.Request(promoteCfg); err != nil {
|
||||
log.Printf("failed to promote user to admin: %v", err)
|
||||
h.reply(msg.Chat.ID, "Не удалось назначить тебя администратором. Проверь права бота")
|
||||
return
|
||||
}
|
||||
|
||||
chatName := msg.Chat.Title
|
||||
if strings.TrimSpace(chatName) == "" {
|
||||
chatName = familyName
|
||||
}
|
||||
|
||||
err = h.receiptApi.CreateFamily(ctx, domain.CreateFamilyRequest{
|
||||
Name: familyName,
|
||||
OwnerID: user.ID,
|
||||
TelegramChatID: msg.Chat.ID,
|
||||
TelegramChatName: chatName,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("failed to create family in api: %v", err)
|
||||
h.reply(msg.Chat.ID, fmt.Sprintf("Не удалось создать семью в API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
h.clearFamilyState(msg.From.ID)
|
||||
h.reply(msg.Chat.ID, "Семья создана успешно")
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
api "FamilyHub/src/integrations/familyHub"
|
||||
"FamilyHub/src/integrations/ocr"
|
||||
"sync"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type registrationState struct {
|
||||
AgreementOffered bool
|
||||
AwaitingApproval bool
|
||||
}
|
||||
|
||||
type familyCreationState struct {
|
||||
AwaitingName bool
|
||||
ChatID int64
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
api *tgbotapi.BotAPI
|
||||
ocr ocr.OCR
|
||||
receiptApi api.ApiClient
|
||||
|
||||
registrationMu sync.Mutex
|
||||
registrationState map[int64]registrationState
|
||||
|
||||
familyMu sync.Mutex
|
||||
familyState map[int64]familyCreationState
|
||||
}
|
||||
|
||||
func New(api *tgbotapi.BotAPI, ocrSvc ocr.OCR, receiptClient api.ApiClient) *Handler {
|
||||
return &Handler{
|
||||
api: api,
|
||||
ocr: ocrSvc,
|
||||
receiptApi: receiptClient,
|
||||
registrationState: map[int64]registrationState{},
|
||||
familyState: map[int64]familyCreationState{},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
func (h *Handler) HandleHelp(msg *tgbotapi.Message) {
|
||||
h.reply(msg.Chat.ID, "Доступные команды:\n/start\n/register\n/termsOfService\n/getAgreement\n/createFamily\n/help")
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"FamilyHub/src/utils"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func (h *Handler) HandlePhoto(msg *tgbotapi.Message) {
|
||||
photo := msg.Photo[len(msg.Photo)-1]
|
||||
|
||||
file, err := h.api.GetFile(tgbotapi.FileConfig{FileID: photo.FileID})
|
||||
if err != nil {
|
||||
h.reply(msg.Chat.ID, "Не смог получить файл 😢")
|
||||
return
|
||||
}
|
||||
|
||||
url := file.Link(h.api.Token)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
h.reply(msg.Chat.ID, "Ошибка загрузки изображения")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
imageBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
h.reply(msg.Chat.ID, "Ошибка чтения изображения")
|
||||
return
|
||||
}
|
||||
|
||||
text, err := h.ocr.Recognize(context.Background(), imageBytes)
|
||||
if err != nil {
|
||||
h.reply(msg.Chat.ID, "Ошибка OCR 😢")
|
||||
return
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
h.reply(msg.Chat.ID, "Текст не найден")
|
||||
return
|
||||
}
|
||||
|
||||
receiptMeta := utils.ExtractReceiptMeta(text)
|
||||
payload := domain.AddReceiptRequest{Number: receiptMeta.ReceiptID, Date: receiptMeta.Date}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
txt, err := utils.DecodeQR(imageBytes)
|
||||
println(txt)
|
||||
|
||||
err = h.receiptApi.SendReceipt(ctx, payload)
|
||||
|
||||
reply := "📄 *Результат распознавания*\n\n"
|
||||
if receiptMeta.Date != "" {
|
||||
reply += "📅 Дата: " + receiptMeta.Date + "\n"
|
||||
} else {
|
||||
reply += "📅 Дата: не найдена\n"
|
||||
}
|
||||
if receiptMeta.ReceiptID != "" {
|
||||
reply += "🧾 Номер чека:\n`" + receiptMeta.ReceiptID + "`\n"
|
||||
} else {
|
||||
reply += "🧾 Номер чека: не найден\n"
|
||||
}
|
||||
if err != nil {
|
||||
reply += "Не удалось отправить чек в API " + err.Error()
|
||||
} else {
|
||||
reply += "Чек добавлен в базу"
|
||||
}
|
||||
|
||||
h.replyMarkdown(msg.Chat.ID, reply)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"FamilyHub/src/domain"
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
const agreementConfirmationText = "Я принимаю условия"
|
||||
|
||||
const termsOfServiceText = "Лицензионное соглашение:\n" +
|
||||
"1. Вы подтверждаете согласие на обработку данных.\n" +
|
||||
"2. Вы соглашаетесь с правилами использования FamilyHUB."
|
||||
|
||||
func (h *Handler) HandleRegister(msg *tgbotapi.Message) {
|
||||
if msg.From == nil {
|
||||
h.reply(msg.Chat.ID, "Не удалось определить пользователя Telegram")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
registered, err := h.receiptApi.IsUserRegistered(ctx, msg.From.ID)
|
||||
if err != nil {
|
||||
log.Printf("failed to check registration: %v", err)
|
||||
h.reply(msg.Chat.ID, "Не удалось проверить регистрацию. Попробуйте позже.")
|
||||
return
|
||||
}
|
||||
|
||||
if registered {
|
||||
h.reply(msg.Chat.ID, "Ты уже зарегистрирован. Доступно: /createFamily, /help, /info")
|
||||
return
|
||||
}
|
||||
|
||||
h.setRegistrationState(msg.From.ID, registrationState{AgreementOffered: true})
|
||||
h.reply(msg.Chat.ID, termsOfServiceText+"\n\nЕсли согласен, нажми /getAgreement")
|
||||
}
|
||||
func (h *Handler) HandleAgreementConfirmation(msg *tgbotapi.Message) {
|
||||
if msg.From == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.EqualFold(strings.TrimSpace(msg.Text), agreementConfirmationText) {
|
||||
h.reply(msg.Chat.ID, "Фраза не совпадает. Введи точно: \"Я принимаю условия\"")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := h.receiptApi.RegisterUser(ctx, domain.CreateUserRequest{
|
||||
TelegramID: msg.From.ID,
|
||||
Username: stringPtrOrNil(msg.From.UserName),
|
||||
FirstName: stringPtrOrNil(msg.From.FirstName),
|
||||
LastName: stringPtrOrNil(msg.From.LastName),
|
||||
LanguageCode: stringPtrOrNil(msg.From.LanguageCode),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("failed to register user: %v", err)
|
||||
h.reply(msg.Chat.ID, "Не удалось завершить регистрацию. Попробуй позже.")
|
||||
return
|
||||
}
|
||||
|
||||
h.clearRegistrationState(msg.From.ID)
|
||||
h.reply(msg.Chat.ID, "Регистрация завершена. Доступно: /createFamily, /help, /info")
|
||||
}
|
||||
func (h *Handler) HandleGetAgreement(msg *tgbotapi.Message) {
|
||||
if msg.From == nil {
|
||||
h.reply(msg.Chat.ID, "Не удалось определить пользователя Telegram")
|
||||
return
|
||||
}
|
||||
|
||||
state, ok := h.getRegistrationState(msg.From.ID)
|
||||
if !ok || !state.AgreementOffered {
|
||||
h.reply(msg.Chat.ID, "Сначала запусти /register")
|
||||
return
|
||||
}
|
||||
|
||||
state.AwaitingApproval = true
|
||||
h.setRegistrationState(msg.From.ID, state)
|
||||
h.reply(msg.Chat.ID, "Введи фразу для подтверждения: \"Я принимаю условия\"")
|
||||
}
|
||||
func (h *Handler) HandleTermsOfService(msg *tgbotapi.Message) {
|
||||
h.reply(msg.Chat.ID, termsOfServiceText)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func (h *Handler) reply(chat int64, text string) {
|
||||
m := tgbotapi.NewMessage(chat, text)
|
||||
_, err := h.api.Send(m)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) replyMarkdown(chatID int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ParseMode = tgbotapi.ModeMarkdown
|
||||
h.api.Send(msg)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
|
||||
func (h *Handler) HandleStart(msg *tgbotapi.Message) {
|
||||
h.reply(msg.Chat.ID, "Привет! Я FamilyHUB-бот. Доступно: /register, /termsOfService, /help")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package handlers
|
||||
|
||||
func (h *Handler) setRegistrationState(userID int64, state registrationState) {
|
||||
h.registrationMu.Lock()
|
||||
defer h.registrationMu.Unlock()
|
||||
h.registrationState[userID] = state
|
||||
}
|
||||
|
||||
func (h *Handler) getRegistrationState(userID int64) (registrationState, bool) {
|
||||
h.registrationMu.Lock()
|
||||
defer h.registrationMu.Unlock()
|
||||
state, ok := h.registrationState[userID]
|
||||
return state, ok
|
||||
}
|
||||
|
||||
func (h *Handler) clearRegistrationState(userID int64) {
|
||||
h.registrationMu.Lock()
|
||||
defer h.registrationMu.Unlock()
|
||||
delete(h.registrationState, userID)
|
||||
}
|
||||
|
||||
func (h *Handler) isAwaitingAgreement(userID int64) bool {
|
||||
state, ok := h.getRegistrationState(userID)
|
||||
return ok && state.AwaitingApproval
|
||||
}
|
||||
|
||||
func (h *Handler) setFamilyState(userID int64, state familyCreationState) {
|
||||
h.familyMu.Lock()
|
||||
defer h.familyMu.Unlock()
|
||||
h.familyState[userID] = state
|
||||
}
|
||||
|
||||
func (h *Handler) getFamilyState(userID int64) (familyCreationState, bool) {
|
||||
h.familyMu.Lock()
|
||||
defer h.familyMu.Unlock()
|
||||
state, ok := h.familyState[userID]
|
||||
return state, ok
|
||||
}
|
||||
|
||||
func (h *Handler) clearFamilyState(userID int64) {
|
||||
h.familyMu.Lock()
|
||||
defer h.familyMu.Unlock()
|
||||
delete(h.familyState, userID)
|
||||
}
|
||||
|
||||
func (h *Handler) isAwaitingFamilyName(userID, chatID int64) bool {
|
||||
state, ok := h.getFamilyState(userID)
|
||||
return ok && state.AwaitingName && state.ChatID == chatID
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
func (h *Handler) HandleUnknown(msg *tgbotapi.Message) {
|
||||
if msg.From == nil {
|
||||
return
|
||||
}
|
||||
|
||||
text := strings.TrimSpace(msg.Text)
|
||||
if text == "" || strings.HasPrefix(text, "/") {
|
||||
return
|
||||
}
|
||||
|
||||
if h.isAwaitingAgreement(msg.From.ID) {
|
||||
h.HandleAgreementConfirmation(msg)
|
||||
return
|
||||
}
|
||||
|
||||
if msg.Chat != nil && h.isAwaitingFamilyName(msg.From.ID, msg.Chat.ID) {
|
||||
h.handleCreateFamilyName(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package handlers
|
||||
|
||||
func stringPtrOrNil(value string) *string {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &value
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"FamilyHub/src/bot/handlers"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
handler *handlers.Handler
|
||||
}
|
||||
|
||||
func NewRouter(handler *handlers.Handler) *Router {
|
||||
return &Router{handler: handler}
|
||||
}
|
||||
|
||||
func (r *Router) Handle(update tgbotapi.Update) {
|
||||
if update.Message == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case update.Message.Photo != nil:
|
||||
r.handler.HandlePhoto(update.Message)
|
||||
case update.Message.Text == "/start":
|
||||
r.handler.HandleStart(update.Message)
|
||||
case update.Message.Text == "/register":
|
||||
r.handler.HandleRegister(update.Message)
|
||||
case update.Message.Text == "/termsOfService":
|
||||
r.handler.HandleTermsOfService(update.Message)
|
||||
case update.Message.Text == "/getAgreement":
|
||||
r.handler.HandleGetAgreement(update.Message)
|
||||
case update.Message.Text == "/help":
|
||||
r.handler.HandleHelp(update.Message)
|
||||
case update.Message.Text == "/createFamily":
|
||||
r.handler.HandleCreateFamily(update.Message)
|
||||
default:
|
||||
r.handler.HandleUnknown(update.Message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user