diff --git a/internal/app/app.go b/internal/app/app.go index 2db5c4c..874992e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -1,10 +1,10 @@ package app import ( - "fmt" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger" "github.com/mymmrac/telego" @@ -55,8 +55,8 @@ func (a *App) Conf() *config.Config { return a.Config } -func (a *App) DB() *db.Repositories { - return a.Repositories +func (a *App) DB() fsmcontract.Repositories { + return &DBWrapper{a.Repositories} } func (a *App) Localizer(lang string) locale.Localizer { @@ -122,7 +122,16 @@ func (a *App) longPoll() error { } defer a.Telegram.StopLongPolling() for update := range updates { - fmt.Printf("Update: %+v\n", update) + go func() { + defer func() { + if r := recover(); r != nil { + a.Logger.Errorf("recovered from panic inside the handler: %v", r) + } + }() + if err := a.handler(update).Handle(update); err != nil { + a.Logger.Errorf("error while handling the update: %s", err) + } + }() } return nil } diff --git a/internal/app/db_wrapper.go b/internal/app/db_wrapper.go new file mode 100644 index 0000000..afaf9ba --- /dev/null +++ b/internal/app/db_wrapper.go @@ -0,0 +1,18 @@ +package app + +import ( + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" +) + +type DBWrapper struct { + r *db.Repositories +} + +func (w *DBWrapper) ForUser() fsmcontract.UserRepository { + return w.r.User +} + +func (w *DBWrapper) ForChat() fsmcontract.ChatRepository { + return w.r.Chat +} diff --git a/internal/db/model/chat.go b/internal/db/model/chat.go index 4332efe..81e506d 100644 --- a/internal/db/model/chat.go +++ b/internal/db/model/chat.go @@ -8,8 +8,8 @@ import ( type Chat struct { ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"` - TelegramID uint64 `gorm:"column:telegram_id; not null" json:"telegramId"` - UserID uint64 `gorm:"column:user_id; not null"` + TelegramID int64 `gorm:"column:telegram_id; not null" json:"telegramId"` + UserID int64 `gorm:"column:user_id; not null"` Members ChatMembers `gorm:"column:members; not null" json:"members"` Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"` } @@ -18,7 +18,7 @@ func (Chat) TableName() string { return "chat" } -type ChatMembers []uint64 +type ChatMembers []int64 func (cm *ChatMembers) Scan(value interface{}) error { switch v := value.(type) { @@ -41,8 +41,12 @@ func (cm *ChatMembers) Scan(value interface{}) error { func (cm ChatMembers) Value() (driver.Value, error) { if cm == nil { - return nil, nil + return "[]", nil } jsonData, err := json.Marshal(cm) return string(jsonData), err } + +func (ChatMembers) GormDataType() string { + return "json" +} diff --git a/internal/db/model/integration.go b/internal/db/model/integration.go index af8fd14..2609805 100644 --- a/internal/db/model/integration.go +++ b/internal/db/model/integration.go @@ -9,7 +9,7 @@ import ( type Integration struct { ID uint64 `gorm:"primaryKey; autoIncrement;"` Type IntegrationType `gorm:"column:type; not null"` - ChatID uint64 `gorm:"column:chat_id; not null"` + ChatID int64 `gorm:"column:chat_id; not null"` Params datatypes.JSONMap `gorm:"column:params; not null"` } @@ -60,5 +60,9 @@ func (it IntegrationType) Value() (driver.Value, error) { if ok { return val, nil } - return "", errors.New("invalid IntegrationType") + return InvalidIntegration, nil +} + +func (IntegrationType) GormDataType() string { + return "varchar(24)" } diff --git a/internal/db/model/user.go b/internal/db/model/user.go index 654a833..7558063 100644 --- a/internal/db/model/user.go +++ b/internal/db/model/user.go @@ -2,7 +2,7 @@ package model type User struct { ID uint64 `gorm:"primary_key;auto_increment" json:"id"` - TelegramID uint64 `gorm:"not null" json:"telegram_id"` + TelegramID int64 `gorm:"not null" json:"telegram_id"` Chats []Chat `gorm:"foreignKey:UserID" json:"chats"` } diff --git a/internal/db/repository/chat.go b/internal/db/repository/chat.go index f45fd9d..fcf752e 100644 --- a/internal/db/repository/chat.go +++ b/internal/db/repository/chat.go @@ -15,17 +15,17 @@ func NewChat(db *gorm.DB) *Chat { } func (c *Chat) ByID(id uint64) (*model.Chat, error) { - var chat *model.Chat - if err := c.db.First(&chat, id).Error; err != nil { + var chat model.Chat + if err := c.db.Model(&chat).First(&chat, id).Error; err != nil { return nil, util.HandleRecordNotFound(err) } - return chat, nil + return &chat, nil } func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) { - var chat *model.Chat - if err := c.db.Preload("Integrations").First(&chat, id).Error; err != nil { + var chat model.Chat + if err := c.db.Model(&chat).Preload("Integrations").First(&chat, id).Error; err != nil { return nil, util.HandleRecordNotFound(err) } - return chat, nil + return &chat, nil } diff --git a/internal/db/repository/user.go b/internal/db/repository/user.go index 618eea5..d7fe965 100644 --- a/internal/db/repository/user.go +++ b/internal/db/repository/user.go @@ -15,17 +15,29 @@ func NewUser(db *gorm.DB) *User { } func (u *User) ByID(id uint64) (*model.User, error) { - var user *model.User - if err := u.db.First(&user, id).Error; err != nil { + var user model.User + if err := u.db.Model(&user).First(&user, id).Error; err != nil { return nil, util.HandleRecordNotFound(err) } - return user, nil + return &user, nil +} + +func (u *User) ByTelegramID(id int64) (*model.User, error) { + var user model.User + if err := u.db.Model(model.User{TelegramID: id}).First(&user).Error; err != nil { + return nil, util.HandleRecordNotFound(err) + } + return &user, nil } func (u *User) ByIDWithChats(id uint64) (*model.User, error) { - var user *model.User - if err := u.db.Preload("Chats").First(&user, id).Error; err != nil { + var user model.User + if err := u.db.Model(&user).Preload("Chats").First(&user, id).Error; err != nil { return nil, util.HandleRecordNotFound(err) } - return user, nil + return &user, nil +} + +func (u *User) Save(user *model.User) error { + return u.db.Model(&model.User{}).Save(user).Error } diff --git a/internal/handler/chat_member_updated_handler.go b/internal/handler/chat_member_updated_handler.go index f77a528..84362c4 100644 --- a/internal/handler/chat_member_updated_handler.go +++ b/internal/handler/chat_member_updated_handler.go @@ -1,14 +1,15 @@ package handler import ( + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" "github.com/mymmrac/telego" ) type ChatMemberUpdatedHandler struct { - app App + app fsmcontract.App } -func NewChatMemberUpdatedHandler(app App) *ChatMemberUpdatedHandler { +func NewChatMemberUpdatedHandler(app fsmcontract.App) *ChatMemberUpdatedHandler { return &ChatMemberUpdatedHandler{app: app} } diff --git a/internal/handler/fsm/fsmcontract/context.go b/internal/handler/fsm/fsmcontract/context.go index 5695d9a..3918b90 100644 --- a/internal/handler/fsm/fsmcontract/context.go +++ b/internal/handler/fsm/fsmcontract/context.go @@ -2,7 +2,6 @@ package fsmcontract import ( "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config" - "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" "github.com/mymmrac/telego" "go.uber.org/zap" @@ -17,6 +16,6 @@ type App interface { Log() *zap.SugaredLogger TG() *telego.Bot Conf() *config.Config - DB() *db.Repositories + DB() Repositories Localizer(string) locale.Localizer } diff --git a/internal/handler/fsm/fsmcontract/db.go b/internal/handler/fsm/fsmcontract/db.go new file mode 100644 index 0000000..9b23a7c --- /dev/null +++ b/internal/handler/fsm/fsmcontract/db.go @@ -0,0 +1,15 @@ +package fsmcontract + +import "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model" + +type Repositories interface { + ForUser() UserRepository + ForChat() ChatRepository +} + +type UserRepository interface { + ByTelegramID(id int64) (*model.User, error) + Save(user *model.User) error +} + +type ChatRepository interface{} diff --git a/internal/handler/fsm/machine/dto/payload.go b/internal/handler/fsm/machine/dto/payload.go index 240855c..1c52182 100644 --- a/internal/handler/fsm/machine/dto/payload.go +++ b/internal/handler/fsm/machine/dto/payload.go @@ -1,7 +1,7 @@ package dto import ( - "encoding/json" + "encoding/json" ) type PayloadAction uint8 @@ -24,14 +24,14 @@ const ( ) type Payload struct { - User uint64 `json:"u,omitempty"` - Chat uint64 `json:"c,omitempty"` + User int64 `json:"u,omitempty"` + Chat int64 `json:"c,omitempty"` Action PayloadAction `json:"a"` Data json.RawMessage `json:"d,omitempty"` } type Member struct { - ID uint64 + ID int64 Name string } @@ -44,7 +44,7 @@ type Answer struct { Result bool `json:"r"` } -func NewNextPageMembersPayload(userID, chatID uint64) *Payload { +func NewNextPageMembersPayload(userID, chatID int64) *Payload { return &Payload{ Action: PayloadActionNext, User: userID, @@ -52,7 +52,7 @@ func NewNextPageMembersPayload(userID, chatID uint64) *Payload { } } -func NewPrevPageMembersPayload(userID, chatID uint64) *Payload { +func NewPrevPageMembersPayload(userID, chatID int64) *Payload { return &Payload{ Action: PayloadActionPrevious, User: userID, @@ -60,7 +60,7 @@ func NewPrevPageMembersPayload(userID, chatID uint64) *Payload { } } -func NewAddMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload { +func NewAddMemberPayload(userID, chatID, memberID int64, memberName string) *Payload { return &Payload{ Action: PayloadActionAddMember, User: userID, @@ -69,7 +69,7 @@ func NewAddMemberPayload(userID, chatID, memberID uint64, memberName string) *Pa } } -func NewRemoveMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload { +func NewRemoveMemberPayload(userID, chatID, memberID int64, memberName string) *Payload { return &Payload{ Action: PayloadActionRemoveMember, User: userID, @@ -78,7 +78,7 @@ func NewRemoveMemberPayload(userID, chatID, memberID uint64, memberName string) } } -func NewVotePayload(chatID uint64, vote float32) *Payload { +func NewVotePayload(chatID int64, vote float32) *Payload { return &Payload{ Action: PayloadActionVote, Chat: chatID, @@ -86,7 +86,7 @@ func NewVotePayload(chatID uint64, vote float32) *Payload { } } -func NewConfirmMembersPayload(userID uint64, chatID uint64) *Payload { +func NewConfirmMembersPayload(userID int64, chatID int64) *Payload { return &Payload{ Action: PayloadActionYesNo, User: userID, @@ -95,7 +95,7 @@ func NewConfirmMembersPayload(userID uint64, chatID uint64) *Payload { } } -func NewRedmineQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload { +func NewRedmineQuestionPayload(userID int64, chatID int64, isYes bool) *Payload { return &Payload{ Action: PayloadActionYesNo, User: userID, @@ -104,7 +104,7 @@ func NewRedmineQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payloa } } -func NewRedmineHoursQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload { +func NewRedmineHoursQuestionPayload(userID int64, chatID int64, isYes bool) *Payload { return &Payload{ Action: PayloadActionYesNo, User: userID, diff --git a/internal/handler/fsm/machine/polls.go b/internal/handler/fsm/machine/polls.go index 8559ee8..e4184c0 100644 --- a/internal/handler/fsm/machine/polls.go +++ b/internal/handler/fsm/machine/polls.go @@ -1,8 +1,8 @@ package machine import ( - "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto" - "time" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto" + "time" "github.com/maypok86/otter" ) @@ -25,11 +25,11 @@ type PollState struct { Result PollResult } -var Polls otter.Cache[uint64, PollState] +var Polls otter.Cache[int64, PollState] func init() { - cache, err := otter.MustBuilder[uint64, PollState](10_000). - Cost(func(key uint64, value PollState) uint32 { + cache, err := otter.MustBuilder[int64, PollState](10_000). + Cost(func(key int64, value PollState) uint32 { return 1 }). WithTTL(time.Hour * 24 * 7). diff --git a/internal/handler/fsm/machine/store.go b/internal/handler/fsm/machine/store.go index 2de3147..a5ce660 100644 --- a/internal/handler/fsm/machine/store.go +++ b/internal/handler/fsm/machine/store.go @@ -6,8 +6,8 @@ import ( ) var ( - userMachines = newStore[uint64, fsmcontract.Machine]() - pollMachines = newStore[uint64, fsmcontract.Machine]() + userMachines = newStore[int64, fsmcontract.Machine]() + pollMachines = newStore[int64, fsmcontract.Machine]() ) type store[K comparable, V any] struct { @@ -32,7 +32,7 @@ func (s *store[K, V]) Load(k K) (v V, ok bool) { return } -func ForUser(id uint64) fsmcontract.Machine { +func ForUser(id int64) fsmcontract.Machine { val, ok := userMachines.Load(id) if !ok { machine := newUserMachine() @@ -42,7 +42,7 @@ func ForUser(id uint64) fsmcontract.Machine { return val } -func ForPoll(id uint64) fsmcontract.Machine { +func ForPoll(id int64) fsmcontract.Machine { val, ok := pollMachines.Load(id) if !ok { machine := newPollMachine() diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 2032fa2..1430f06 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,11 +1,7 @@ package handler import ( - "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config" - "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db" - "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" "github.com/mymmrac/telego" - "go.uber.org/zap" ) type Type uint8 @@ -16,14 +12,6 @@ const ( ChatMemberUpdated ) -type App interface { - Log() *zap.SugaredLogger - TG() *telego.Bot - Conf() *config.Config - DB() *db.Repositories - Localizer(string) locale.Localizer -} - type Handler interface { Handle(update telego.Update) error } diff --git a/internal/handler/iface/base.go b/internal/handler/iface/base.go new file mode 100644 index 0000000..c03d5eb --- /dev/null +++ b/internal/handler/iface/base.go @@ -0,0 +1,28 @@ +package iface + +import ( + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" +) + +type Base struct { + App fsmcontract.App + UserID int64 + ChatID int64 +} + +func NewBase(app fsmcontract.App, userID int64, chatID int64) Base { + return Base{app, userID, chatID} +} + +func (b Base) Localizer(lang string) locale.Localizer { + if len(lang) > 2 { + lang = lang[:2] + } + switch lang { + case "en", "ru": + return b.App.Localizer(lang) + default: + return b.App.Localizer("en") + } +} diff --git a/internal/handler/new_message_handler.go b/internal/handler/new_message_handler.go index 933b8c2..922f3b2 100644 --- a/internal/handler/new_message_handler.go +++ b/internal/handler/new_message_handler.go @@ -1,18 +1,35 @@ package handler import ( + "encoding/json" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard" "github.com/mymmrac/telego" ) type MessageHandler struct { - app App + app fsmcontract.App } -func NewMessageHandler(app App) Handler { +func NewMessageHandler(app fsmcontract.App) Handler { return &MessageHandler{app: app} } -func (h *MessageHandler) Handle(telego.Update) error { - // TODO implement me - panic("implement me") +func (h *MessageHandler) Handle(wh telego.Update) error { + if wh.Message != nil { + if wh.Message.From != nil && + wh.Message.Chat.Type == telego.ChatTypePrivate && + util.MatchCommand("start", wh.Message) { + return wizard.NewRegister(h.app, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh) + } + + // TODO: Remove debug statement below. + h.app.Log().Debugf("New Message: %s", func(msg *telego.Message) string { + data, _ := json.Marshal(msg) + return string(data) + }(wh.Message)) + } + + return nil } diff --git a/internal/handler/util/command.go b/internal/handler/util/command.go new file mode 100644 index 0000000..a98c293 --- /dev/null +++ b/internal/handler/util/command.go @@ -0,0 +1,10 @@ +package util + +import ( + "github.com/mymmrac/telego" + th "github.com/mymmrac/telego/telegohandler" +) + +func MatchCommand(command string, msg *telego.Message) bool { + return th.CommandEqual(command)(telego.Update{Message: msg}) +} diff --git a/internal/handler/wizard/register.go b/internal/handler/wizard/register.go new file mode 100644 index 0000000..8e6692c --- /dev/null +++ b/internal/handler/wizard/register.go @@ -0,0 +1,54 @@ +package wizard + +import ( + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract" + "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface" + "github.com/mymmrac/telego" + tu "github.com/mymmrac/telego/telegoutil" +) + +type Register struct { + iface.Base +} + +func NewRegister(app fsmcontract.App, userID, chatID int64) *Register { + return &Register{iface.NewBase(app, userID, chatID)} +} + +func (h *Register) Handle(wh telego.Update) error { + loc := h.Localizer(wh.Message.From.LanguageCode) + userRepo := h.App.DB().ForUser() + user, err := userRepo.ByTelegramID(wh.Message.From.ID) + if err != nil { + return err + } + + if user != nil && user.ID > 0 { + _, err := h.App.TG().SendMessage(&telego.SendMessageParams{ + ChatID: tu.ID(wh.Message.Chat.ID), + Text: loc.Message("welcome"), + ParseMode: telego.ModeMarkdown, + }) + return err + } + + err = userRepo.Save(&model.User{ + TelegramID: wh.Message.From.ID, + }) + if err != nil { + _, _ = h.App.TG().SendMessage(&telego.SendMessageParams{ + ChatID: tu.ID(wh.Message.Chat.ID), + Text: loc.Message("internal_error"), + ParseMode: telego.ModeMarkdown, + }) + return err + } + + _, err = h.App.TG().SendMessage(&telego.SendMessageParams{ + ChatID: tu.ID(wh.Message.Chat.ID), + Text: loc.Message("welcome"), + ParseMode: telego.ModeMarkdown, + }) + return err +} diff --git a/internal/locale/app.en.yaml b/internal/locale/app.en.yaml index dd8cd1a..5e157b9 100644 --- a/internal/locale/app.en.yaml +++ b/internal/locale/app.en.yaml @@ -1,4 +1,5 @@ welcome: "👋 Hello! This bot allows you to conduct Scrum Poker directly in a Telegram chat and pass results to issues tied to a Redmine instance. To start, add the bot to the chat where you want to conduct poker. After this, you can continue with the setup." +internal_error: "❌ Internal error, try again later." bot_was_added: "Great, the bot has been added to the chat \"{{.Name}}\". Choose chat members who will participate in the poker." previous: "◀️ Back" next: "Next ▶️" diff --git a/internal/locale/app.ru.yaml b/internal/locale/app.ru.yaml index 297d3c6..69a06a0 100644 --- a/internal/locale/app.ru.yaml +++ b/internal/locale/app.ru.yaml @@ -1,4 +1,5 @@ welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку." +internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже." bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере." previous: "◀️ Назад" next: "Вперед ▶️"