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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
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,

View File

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

View File

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

View File

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

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

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."
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 ▶️"

View File

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