first webhook processor, fix errors

This commit is contained in:
Pavel 2024-05-09 17:42:42 +03:00
parent 0e2cc70a09
commit 1e6407adfe
20 changed files with 226 additions and 65 deletions

View File

@ -1,10 +1,10 @@
package app package app
import ( import (
"fmt"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler" "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/locale"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
@ -55,8 +55,8 @@ func (a *App) Conf() *config.Config {
return a.Config return a.Config
} }
func (a *App) DB() *db.Repositories { func (a *App) DB() fsmcontract.Repositories {
return a.Repositories return &DBWrapper{a.Repositories}
} }
func (a *App) Localizer(lang string) locale.Localizer { func (a *App) Localizer(lang string) locale.Localizer {
@ -122,7 +122,16 @@ func (a *App) longPoll() error {
} }
defer a.Telegram.StopLongPolling() defer a.Telegram.StopLongPolling()
for update := range updates { 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 return nil
} }

View File

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

View File

@ -8,8 +8,8 @@ import (
type Chat struct { type Chat struct {
ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"` ID uint64 `gorm:"primaryKey; autoIncrement" json:"id"`
TelegramID uint64 `gorm:"column:telegram_id; not null" json:"telegramId"` TelegramID int64 `gorm:"column:telegram_id; not null" json:"telegramId"`
UserID uint64 `gorm:"column:user_id; not null"` UserID int64 `gorm:"column:user_id; not null"`
Members ChatMembers `gorm:"column:members; not null" json:"members"` Members ChatMembers `gorm:"column:members; not null" json:"members"`
Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"` Integrations []Integration `gorm:"foreignKey:ChatID" json:"integrations"`
} }
@ -18,7 +18,7 @@ func (Chat) TableName() string {
return "chat" return "chat"
} }
type ChatMembers []uint64 type ChatMembers []int64
func (cm *ChatMembers) Scan(value interface{}) error { func (cm *ChatMembers) Scan(value interface{}) error {
switch v := value.(type) { switch v := value.(type) {
@ -41,8 +41,12 @@ func (cm *ChatMembers) Scan(value interface{}) error {
func (cm ChatMembers) Value() (driver.Value, error) { func (cm ChatMembers) Value() (driver.Value, error) {
if cm == nil { if cm == nil {
return nil, nil return "[]", nil
} }
jsonData, err := json.Marshal(cm) jsonData, err := json.Marshal(cm)
return string(jsonData), err return string(jsonData), err
} }
func (ChatMembers) GormDataType() string {
return "json"
}

View File

@ -9,7 +9,7 @@ import (
type Integration struct { type Integration struct {
ID uint64 `gorm:"primaryKey; autoIncrement;"` ID uint64 `gorm:"primaryKey; autoIncrement;"`
Type IntegrationType `gorm:"column:type; not null"` 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"` Params datatypes.JSONMap `gorm:"column:params; not null"`
} }
@ -60,5 +60,9 @@ func (it IntegrationType) Value() (driver.Value, error) {
if ok { if ok {
return val, nil return val, nil
} }
return "", errors.New("invalid IntegrationType") return InvalidIntegration, nil
}
func (IntegrationType) GormDataType() string {
return "varchar(24)"
} }

View File

@ -2,7 +2,7 @@ package model
type User struct { type User struct {
ID uint64 `gorm:"primary_key;auto_increment" json:"id"` 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"` Chats []Chat `gorm:"foreignKey:UserID" json:"chats"`
} }

View File

@ -15,17 +15,17 @@ func NewChat(db *gorm.DB) *Chat {
} }
func (c *Chat) ByID(id uint64) (*model.Chat, error) { func (c *Chat) ByID(id uint64) (*model.Chat, error) {
var chat *model.Chat var chat model.Chat
if err := c.db.First(&chat, id).Error; err != nil { if err := c.db.Model(&chat).First(&chat, id).Error; err != nil {
return nil, util.HandleRecordNotFound(err) return nil, util.HandleRecordNotFound(err)
} }
return chat, nil return &chat, nil
} }
func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) { func (c *Chat) ByIDWithIntegrations(id uint64) (*model.Chat, error) {
var chat *model.Chat var chat model.Chat
if err := c.db.Preload("Integrations").First(&chat, id).Error; err != nil { if err := c.db.Model(&chat).Preload("Integrations").First(&chat, id).Error; err != nil {
return nil, util.HandleRecordNotFound(err) return nil, util.HandleRecordNotFound(err)
} }
return chat, nil return &chat, nil
} }

View File

@ -15,17 +15,29 @@ func NewUser(db *gorm.DB) *User {
} }
func (u *User) ByID(id uint64) (*model.User, error) { func (u *User) ByID(id uint64) (*model.User, error) {
var user *model.User var user model.User
if err := u.db.First(&user, id).Error; err != nil { if err := u.db.Model(&user).First(&user, id).Error; err != nil {
return nil, util.HandleRecordNotFound(err) 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) { func (u *User) ByIDWithChats(id uint64) (*model.User, error) {
var user *model.User var user model.User
if err := u.db.Preload("Chats").First(&user, id).Error; err != nil { if err := u.db.Model(&user).Preload("Chats").First(&user, id).Error; err != nil {
return nil, util.HandleRecordNotFound(err) 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
} }

View File

@ -1,14 +1,15 @@
package handler package handler
import ( import (
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
) )
type ChatMemberUpdatedHandler struct { type ChatMemberUpdatedHandler struct {
app App app fsmcontract.App
} }
func NewChatMemberUpdatedHandler(app App) *ChatMemberUpdatedHandler { func NewChatMemberUpdatedHandler(app fsmcontract.App) *ChatMemberUpdatedHandler {
return &ChatMemberUpdatedHandler{app: app} return &ChatMemberUpdatedHandler{app: app}
} }

View File

@ -2,7 +2,6 @@ package fsmcontract
import ( import (
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
"go.uber.org/zap" "go.uber.org/zap"
@ -17,6 +16,6 @@ type App interface {
Log() *zap.SugaredLogger Log() *zap.SugaredLogger
TG() *telego.Bot TG() *telego.Bot
Conf() *config.Config Conf() *config.Config
DB() *db.Repositories DB() Repositories
Localizer(string) locale.Localizer Localizer(string) locale.Localizer
} }

View File

@ -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{}

View File

@ -1,7 +1,7 @@
package dto package dto
import ( import (
"encoding/json" "encoding/json"
) )
type PayloadAction uint8 type PayloadAction uint8
@ -24,14 +24,14 @@ const (
) )
type Payload struct { type Payload struct {
User uint64 `json:"u,omitempty"` User int64 `json:"u,omitempty"`
Chat uint64 `json:"c,omitempty"` Chat int64 `json:"c,omitempty"`
Action PayloadAction `json:"a"` Action PayloadAction `json:"a"`
Data json.RawMessage `json:"d,omitempty"` Data json.RawMessage `json:"d,omitempty"`
} }
type Member struct { type Member struct {
ID uint64 ID int64
Name string Name string
} }
@ -44,7 +44,7 @@ type Answer struct {
Result bool `json:"r"` Result bool `json:"r"`
} }
func NewNextPageMembersPayload(userID, chatID uint64) *Payload { func NewNextPageMembersPayload(userID, chatID int64) *Payload {
return &Payload{ return &Payload{
Action: PayloadActionNext, Action: PayloadActionNext,
User: userID, 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{ return &Payload{
Action: PayloadActionPrevious, Action: PayloadActionPrevious,
User: userID, 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{ return &Payload{
Action: PayloadActionAddMember, Action: PayloadActionAddMember,
User: userID, 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{ return &Payload{
Action: PayloadActionRemoveMember, Action: PayloadActionRemoveMember,
User: userID, 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{ return &Payload{
Action: PayloadActionVote, Action: PayloadActionVote,
Chat: chatID, 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{ return &Payload{
Action: PayloadActionYesNo, Action: PayloadActionYesNo,
User: userID, 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{ return &Payload{
Action: PayloadActionYesNo, Action: PayloadActionYesNo,
User: userID, 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{ return &Payload{
Action: PayloadActionYesNo, Action: PayloadActionYesNo,
User: userID, User: userID,

View File

@ -1,8 +1,8 @@
package machine package machine
import ( import (
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto" "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto"
"time" "time"
"github.com/maypok86/otter" "github.com/maypok86/otter"
) )
@ -25,11 +25,11 @@ type PollState struct {
Result PollResult Result PollResult
} }
var Polls otter.Cache[uint64, PollState] var Polls otter.Cache[int64, PollState]
func init() { func init() {
cache, err := otter.MustBuilder[uint64, PollState](10_000). cache, err := otter.MustBuilder[int64, PollState](10_000).
Cost(func(key uint64, value PollState) uint32 { Cost(func(key int64, value PollState) uint32 {
return 1 return 1
}). }).
WithTTL(time.Hour * 24 * 7). WithTTL(time.Hour * 24 * 7).

View File

@ -6,8 +6,8 @@ import (
) )
var ( var (
userMachines = newStore[uint64, fsmcontract.Machine]() userMachines = newStore[int64, fsmcontract.Machine]()
pollMachines = newStore[uint64, fsmcontract.Machine]() pollMachines = newStore[int64, fsmcontract.Machine]()
) )
type store[K comparable, V any] struct { type store[K comparable, V any] struct {
@ -32,7 +32,7 @@ func (s *store[K, V]) Load(k K) (v V, ok bool) {
return return
} }
func ForUser(id uint64) fsmcontract.Machine { func ForUser(id int64) fsmcontract.Machine {
val, ok := userMachines.Load(id) val, ok := userMachines.Load(id)
if !ok { if !ok {
machine := newUserMachine() machine := newUserMachine()
@ -42,7 +42,7 @@ func ForUser(id uint64) fsmcontract.Machine {
return val return val
} }
func ForPoll(id uint64) fsmcontract.Machine { func ForPoll(id int64) fsmcontract.Machine {
val, ok := pollMachines.Load(id) val, ok := pollMachines.Load(id)
if !ok { if !ok {
machine := newPollMachine() machine := newPollMachine()

View File

@ -1,11 +1,7 @@
package handler package handler
import ( 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" "github.com/mymmrac/telego"
"go.uber.org/zap"
) )
type Type uint8 type Type uint8
@ -16,14 +12,6 @@ const (
ChatMemberUpdated ChatMemberUpdated
) )
type App interface {
Log() *zap.SugaredLogger
TG() *telego.Bot
Conf() *config.Config
DB() *db.Repositories
Localizer(string) locale.Localizer
}
type Handler interface { type Handler interface {
Handle(update telego.Update) error Handle(update telego.Update) error
} }

View File

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

View File

@ -1,18 +1,35 @@
package handler package handler
import ( 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" "github.com/mymmrac/telego"
) )
type MessageHandler struct { type MessageHandler struct {
app App app fsmcontract.App
} }
func NewMessageHandler(app App) Handler { func NewMessageHandler(app fsmcontract.App) Handler {
return &MessageHandler{app: app} return &MessageHandler{app: app}
} }
func (h *MessageHandler) Handle(telego.Update) error { func (h *MessageHandler) Handle(wh telego.Update) error {
// TODO implement me if wh.Message != nil {
panic("implement me") 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
} }

View File

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

View File

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

View File

@ -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." 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." bot_was_added: "Great, the bot has been added to the chat \"{{.Name}}\". Choose chat members who will participate in the poker."
previous: "◀️ Back" previous: "◀️ Back"
next: "Next ▶️" next: "Next ▶️"

View File

@ -1,4 +1,5 @@
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку." welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
internal_error: "❌ Внутренняя ошибка, попробуйте попытку позже."
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере." bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
previous: "◀️ Назад" previous: "◀️ Назад"
next: "Вперед ▶️" next: "Вперед ▶️"