wip: moving to fsm
This commit is contained in:
parent
49cfb9c649
commit
a2e74105c5
10
.air.toml
Normal file
10
.air.toml
Normal file
@ -0,0 +1,10 @@
|
||||
root = "."
|
||||
|
||||
[build]
|
||||
exclude_regex = ["_test\\.go"]
|
||||
bin = "/usr/bin/make debug"
|
||||
cmd = "/usr/bin/make build"
|
||||
kill_delay = 0
|
||||
log = "build-errors.log"
|
||||
send_interrupt = true
|
||||
stop_on_error = true
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,6 +22,8 @@ vendor/
|
||||
# Go workspace file
|
||||
go.work
|
||||
.idea/
|
||||
.config/
|
||||
tmp/
|
||||
|
||||
# Env files
|
||||
.env
|
||||
|
21
Makefile
21
Makefile
@ -2,20 +2,37 @@ SHELL = /bin/bash -o pipefail
|
||||
export PATH := $(shell go env GOPATH)/bin:$(PATH)
|
||||
|
||||
ROOT_DIR=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
BIN=$(ROOT_DIR)/build/vegapokerbot
|
||||
BIN_NAME=vegapokerbot
|
||||
BIN=$(ROOT_DIR)/build/$(BIN_NAME)
|
||||
GO_VERSION=$(shell go version | sed -e 's/go version //')
|
||||
BIN_DIR=$(ROOT_DIR)/build
|
||||
|
||||
build: deps fmt
|
||||
@echo "> building with ${GO_VERSION}"
|
||||
ifeq ($(DEBUG), true)
|
||||
@echo "> building debug with ${GO_VERSION}"
|
||||
@CGO_ENABLED=0 go build -buildvcs=false -gcflags "all=-N -l" -tags=release -o $(BIN) .
|
||||
else
|
||||
@echo "> building release with ${GO_VERSION}"
|
||||
@CGO_ENABLED=0 go build -buildvcs=false -tags=release -o $(BIN) .
|
||||
endif
|
||||
@echo $(BIN)
|
||||
|
||||
docker:
|
||||
@docker buildx build --platform linux/amd64 --tag $CI_APP_IMAGE --file Dockerfile .
|
||||
|
||||
run:
|
||||
ifeq ($(DEBUG), true)
|
||||
@killall -s 9 dlv > /dev/null 2>&1 || true
|
||||
@killall -s 9 ${BIN_NAME} > /dev/null 2>&1 || true
|
||||
@air
|
||||
else
|
||||
@${BIN} run
|
||||
endif
|
||||
|
||||
debug:
|
||||
@echo "> starting under delve"
|
||||
@dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient --log exec ${BIN} run
|
||||
|
||||
fmt:
|
||||
@echo "> fmt"
|
||||
@gofmt -l -s -w `go list -buildvcs=false -f '{{.Dir}}' ${ROOT_DIR}/... | grep -v /vendor/`
|
||||
|
@ -22,8 +22,10 @@ services:
|
||||
- db
|
||||
environment:
|
||||
GOCACHE: /go
|
||||
DEBUG: true # Set to false to disable hot-reload & debugger.
|
||||
ports:
|
||||
- 3333:3333
|
||||
- 40000:40000
|
||||
command: make build run
|
||||
|
||||
networks:
|
||||
|
3
go.mod
3
go.mod
@ -12,6 +12,7 @@ require (
|
||||
github.com/maypok86/otter v1.2.1
|
||||
github.com/mymmrac/telego v0.29.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/text v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
@ -26,6 +27,7 @@ require (
|
||||
github.com/bytedance/sonic v1.11.3 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dolthub/maphash v0.1.0 // indirect
|
||||
github.com/fasthttp/router v1.5.0 // indirect
|
||||
github.com/gammazero/deque v0.2.1 // indirect
|
||||
@ -42,6 +44,7 @@ require (
|
||||
github.com/klauspost/compress v1.17.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
@ -8,10 +8,12 @@ import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/types"
|
||||
"github.com/mymmrac/telego"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@ -21,6 +23,7 @@ type App struct {
|
||||
Config *config.Config
|
||||
Repositories *db.Repositories
|
||||
Handlers map[handler.Type]handler.Handler
|
||||
updates types.Queue[*telego.Update]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
@ -114,6 +117,7 @@ func (a *App) initTelegram() error {
|
||||
}
|
||||
|
||||
func (a *App) initHandlers() {
|
||||
a.updates = types.NewQueue[*telego.Update]()
|
||||
fsmwizard.PopulateStates(a)
|
||||
a.Handlers = map[handler.Type]handler.Handler{
|
||||
handler.Noop: handler.NewNoopHandler(a.Logger, a.Config.Debug),
|
||||
@ -140,6 +144,17 @@ func (a *App) handler(update telego.Update) handler.Handler {
|
||||
return a.Handlers[handler.Noop]
|
||||
}
|
||||
|
||||
func (a *App) processUpdates() {
|
||||
for {
|
||||
item := a.updates.Dequeue()
|
||||
if item == nil {
|
||||
time.Sleep(time.Nanosecond * 200)
|
||||
continue
|
||||
}
|
||||
a.processUpdate(*item)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) processUpdate(update telego.Update) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@ -157,9 +172,11 @@ func (a *App) longPoll() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go a.processUpdates()
|
||||
defer a.Telegram.StopLongPolling()
|
||||
for update := range updates {
|
||||
go a.processUpdate(update)
|
||||
update := update
|
||||
a.updates.Enqueue(&update)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -184,8 +201,10 @@ func (a *App) listenWebhook() error {
|
||||
defer func() {
|
||||
_ = a.Telegram.StopWebhook()
|
||||
}()
|
||||
go a.processUpdates()
|
||||
for update := range updates {
|
||||
go a.processUpdate(update)
|
||||
update := update
|
||||
a.updates.Enqueue(&update)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsmwizard"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
@ -21,18 +22,25 @@ func NewChatMemberUpdatedHandler(app iface.App) *ChatMemberUpdatedHandler {
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) Handle(wh telego.Update) error {
|
||||
cm := wh.MyChatMember
|
||||
if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
return h.handleAddToChat(wh.MyChatMember.Chat)
|
||||
}
|
||||
if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
!cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
return h.handleRemoveFromChat(wh.MyChatMember.Chat)
|
||||
}
|
||||
|
||||
h.leaveChat(wh.MyChatMember.Chat.ID)
|
||||
return nil
|
||||
return fsmwizard.Get(wh.MyChatMember.From.ID).Handle(func(w *fsmwizard.Wizard) *fsmwizard.Wizard {
|
||||
if w == nil {
|
||||
return &fsmwizard.Wizard{Data: wh}
|
||||
}
|
||||
w.Data = wh
|
||||
return w
|
||||
})
|
||||
// cm := wh.MyChatMember
|
||||
// if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
// cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
// return h.handleAddToChat(wh.MyChatMember.Chat)
|
||||
// }
|
||||
// if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == h.App.TGProfile().ID &&
|
||||
// !cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == h.App.TGProfile().ID {
|
||||
// return h.handleRemoveFromChat(wh.MyChatMember.Chat)
|
||||
// }
|
||||
//
|
||||
// h.leaveChat(wh.MyChatMember.Chat.ID)
|
||||
// return nil
|
||||
}
|
||||
|
||||
func (h *ChatMemberUpdatedHandler) handleAddToChat(tgChat telego.Chat) error {
|
||||
|
@ -22,14 +22,14 @@ func NewAddChatMemberState(app iface.App) fsm.IState[Wizard] {
|
||||
return &AddChatMemberState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *AddChatMemberState) Enter(pl *Wizard, mc fsm.MachineControls[*Wizard]) error {
|
||||
func (s *AddChatMemberState) Enter(pl *Wizard, mc fsm.MachineControls[Wizard]) error {
|
||||
if pl.Data.MyChatMember == nil {
|
||||
s.Move(mc, WaitingForMemberWebhookStateID, pl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AddChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
func (s *AddChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
next := WaitingForMemberWebhookStateID
|
||||
defer func() {
|
||||
s.Move(mc, next, pl)
|
||||
@ -78,7 +78,11 @@ func (s *AddChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard])
|
||||
})
|
||||
s.LogError(err)
|
||||
pl.User = user
|
||||
pl.UserID = user.TelegramID
|
||||
pl.Chat = chat
|
||||
pl.ChatID = tgChat.ID
|
||||
pl.TGChat = tgChat
|
||||
pl.IsMaster = true
|
||||
next = KeyboardChooserStateID
|
||||
}
|
||||
|
||||
@ -105,14 +109,7 @@ func (s *AddChatMemberState) handleTooManyMembers(chatID int64) bool {
|
||||
}
|
||||
|
||||
func (s *AddChatMemberState) getUserAndChat(tgChat telego.Chat) (*model.User, *model.Chat) {
|
||||
cr := s.App.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||
if err != nil {
|
||||
s.curseAndLeave(tgChat.ID, err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := s.getRegisteredAdmin(tgChat.ID)
|
||||
user, err := s.App.DB().ForUser().ByTelegramID(s.Payload.Data.MyChatMember.From.ID)
|
||||
if err != nil {
|
||||
s.curseAndLeave(tgChat.ID, err)
|
||||
return nil, nil
|
||||
@ -128,6 +125,12 @@ func (s *AddChatMemberState) getUserAndChat(tgChat telego.Chat) (*model.User, *m
|
||||
s.LogError(err)
|
||||
return nil, nil
|
||||
}
|
||||
cr := s.App.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(tgChat.ID)
|
||||
if err != nil {
|
||||
s.curseAndLeave(tgChat.ID, err)
|
||||
return nil, nil
|
||||
}
|
||||
return user, chat
|
||||
}
|
||||
|
||||
@ -137,27 +140,6 @@ func (s *AddChatMemberState) curseAndLeave(chatID int64, err error) {
|
||||
s.LogError(err)
|
||||
}
|
||||
|
||||
func (s *AddChatMemberState) getRegisteredAdmin(chatID int64) (*model.User, error) {
|
||||
admins, err := s.App.TG().GetChatAdministrators(&telego.GetChatAdministratorsParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adminIDs := make([]int64, len(admins))
|
||||
for i, admin := range admins {
|
||||
adminIDs[i] = admin.MemberUser().ID
|
||||
}
|
||||
dbAdmins, err := s.App.DB().ForUser().ByTelegramIDs(adminIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dbAdmins) > 0 {
|
||||
return &dbAdmins[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *AddChatMemberState) leaveChat(chatID int64) {
|
||||
s.LogError(s.App.TG().LeaveChat(&telego.LeaveChatParams{
|
||||
ChatID: tu.ID(chatID),
|
||||
|
@ -3,6 +3,7 @@ package fsmwizard
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db/model"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/fsm"
|
||||
"github.com/mymmrac/telego"
|
||||
@ -11,12 +12,15 @@ import (
|
||||
)
|
||||
|
||||
type Wizard struct {
|
||||
UserID int64
|
||||
ChatID int64
|
||||
TGChat telego.Chat
|
||||
User *model.User
|
||||
Loc locale.Localizer
|
||||
Data telego.Update
|
||||
UserID int64
|
||||
ChatID int64
|
||||
TGChat telego.Chat
|
||||
User *model.User
|
||||
Chat *model.Chat
|
||||
Loc locale.Localizer
|
||||
Data telego.Update
|
||||
KeyClick util.Payload
|
||||
IsMaster bool
|
||||
}
|
||||
|
||||
type State struct {
|
||||
@ -33,16 +37,16 @@ func (s State) Localizer(langCode ...string) locale.Localizer {
|
||||
if len(langCode) > 0 {
|
||||
lang = langCode[0]
|
||||
} else {
|
||||
if s.Payload.User != nil {
|
||||
if s.Payload != nil && s.Payload.User != nil {
|
||||
lang = s.Payload.User.Language
|
||||
} else if s.Payload.UserID > 0 {
|
||||
} else if s.Payload != nil && s.Payload.UserID > 0 {
|
||||
user, _ := s.App.DB().ForUser().ByTelegramID(s.Payload.UserID)
|
||||
if user != nil && user.Language != "" {
|
||||
lang = user.Language
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.Payload.Loc != nil && (len(langCode) == 0 || s.Payload.Loc.Tag().String() == lang) {
|
||||
if s.Payload != nil && s.Payload.Loc != nil && (len(langCode) == 0 || s.hasLocalizerWithLocale(lang)) {
|
||||
return s.Payload.Loc
|
||||
}
|
||||
lang = strings.ToLower(lang)
|
||||
@ -57,6 +61,13 @@ func (s State) Localizer(langCode ...string) locale.Localizer {
|
||||
}
|
||||
}
|
||||
|
||||
func (s State) hasLocalizerWithLocale(lang string) bool {
|
||||
if s.Payload.Loc == nil {
|
||||
return false
|
||||
}
|
||||
return s.Payload.Loc.Tag().String() == lang
|
||||
}
|
||||
|
||||
func (s State) LogError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
@ -68,6 +79,6 @@ func (s State) LogError(err error) {
|
||||
s.App.Log().Errorf("handler error: %s, user id: %d, chat id: %d", err, s.Payload.UserID, s.Payload.ChatID)
|
||||
}
|
||||
|
||||
func (s State) Move(mc fsm.MachineControls[*Wizard], stateID fsm.StateID, pl *Wizard) {
|
||||
func (s State) Move(mc fsm.MachineControls[Wizard], stateID fsm.StateID, pl *Wizard) {
|
||||
s.LogError(mc.Move(stateID, pl))
|
||||
}
|
||||
|
47
internal/handler/fsmwizard/configure_redmine_query_state.go
Normal file
47
internal/handler/fsmwizard/configure_redmine_query_state.go
Normal file
@ -0,0 +1,47 @@
|
||||
package fsmwizard
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/fsm"
|
||||
"github.com/mymmrac/telego"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
)
|
||||
|
||||
const ConfigureRedmineQueryStateID fsm.StateID = "configure_redmine_query"
|
||||
|
||||
type ConfigureRedmineQueryState struct {
|
||||
State
|
||||
}
|
||||
|
||||
func NewConfigureRedmineQueryState(app iface.App) fsm.IState[Wizard] {
|
||||
return &ConfigureRedmineQueryState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *ConfigureRedmineQueryState) Enter(pl *Wizard, _ fsm.MachineControls[Wizard]) error {
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(pl.User.ChatID),
|
||||
Text: pl.Loc.Message("ask_for_redmine"),
|
||||
ParseMode: telego.ModeMarkdown,
|
||||
ReplyMarkup: &telego.InlineKeyboardMarkup{InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||
{
|
||||
{
|
||||
Text: pl.Loc.Message("yes"),
|
||||
CallbackData: util.NewRedmineQuestionPayload(
|
||||
pl.User.TelegramID, pl.Chat.TelegramID, true).String(),
|
||||
},
|
||||
{
|
||||
Text: pl.Loc.Message("no"),
|
||||
CallbackData: util.NewRedmineQuestionPayload(
|
||||
pl.User.TelegramID, pl.Chat.TelegramID, false).String(),
|
||||
},
|
||||
},
|
||||
}},
|
||||
})
|
||||
s.LogError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ConfigureRedmineQueryState) ID() fsm.StateID {
|
||||
return ConfigureRedmineQueryStateID
|
||||
}
|
@ -14,7 +14,7 @@ func NewErrorState(log *zap.SugaredLogger) *ErrorState {
|
||||
return &ErrorState{log: log}
|
||||
}
|
||||
|
||||
func (s *ErrorState) Handle(err error, cur fsm.StateID, next fsm.StateID, pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
func (s *ErrorState) Handle(err error, cur fsm.StateID, next fsm.StateID, pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
s.log.Errorf("critical wizard error: %s, current state: %s, next state: %s, payload: %#v",
|
||||
err, cur, next, pl)
|
||||
mc.Reset()
|
||||
|
@ -17,7 +17,7 @@ func NewHelpState(app iface.App) fsm.IState[Wizard] {
|
||||
return &HelpState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *HelpState) Enter(pl *Wizard, _ fsm.MachineControls[*Wizard]) error {
|
||||
func (s *HelpState) Enter(pl *Wizard, _ fsm.MachineControls[Wizard]) error {
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(pl.Data.Message.Chat.ID),
|
||||
Text: s.Localizer(pl.Data.Message.From.LanguageCode).
|
||||
|
@ -1,35 +1,27 @@
|
||||
package fsmwizard
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/types"
|
||||
"time"
|
||||
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/fsm"
|
||||
"github.com/maypok86/otter"
|
||||
)
|
||||
|
||||
var (
|
||||
wizards otter.Cache[int64, fsm.IMachine[Wizard]]
|
||||
wizards *types.TTLMap[int64, fsm.IMachine[Wizard]]
|
||||
states []fsm.IState[Wizard]
|
||||
errorState *ErrorState
|
||||
appPointer iface.App
|
||||
)
|
||||
|
||||
func init() {
|
||||
storage, err := otter.MustBuilder[int64, fsm.IMachine[Wizard]](1000).
|
||||
Cost(func(key int64, value fsm.IMachine[Wizard]) uint32 {
|
||||
return 1
|
||||
}).
|
||||
WithTTL(time.Hour * 24 * 7).
|
||||
Build()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
wizards = storage
|
||||
wizards = types.NewTTLMap[int64, fsm.IMachine[Wizard]](time.Hour * 24 * 7)
|
||||
}
|
||||
|
||||
func Get(userID int64) fsm.IMachine[Wizard] {
|
||||
if machine, ok := wizards.Get(userID); ok {
|
||||
if machine, ok := wizards.Get(userID); ok && machine != nil {
|
||||
return machine
|
||||
}
|
||||
machine := newWizard()
|
||||
@ -38,16 +30,18 @@ func Get(userID int64) fsm.IMachine[Wizard] {
|
||||
}
|
||||
|
||||
func newWizard() fsm.IMachine[Wizard] {
|
||||
return fsm.New[Wizard](states[0].ID(), Wizard{}, wizardPreHandle, states, errorState)
|
||||
return fsm.New[Wizard](states[0].ID(), wizardRouter, states, errorState)
|
||||
}
|
||||
|
||||
// PopulateStates will init all state handlers for future use.
|
||||
func PopulateStates(app iface.App) {
|
||||
appPointer = app
|
||||
states = []fsm.IState[Wizard]{
|
||||
NewRegisterState(app),
|
||||
NewWaitingForMemberWebhookState(app),
|
||||
NewAddChatMemberState(app),
|
||||
NewKeyboardChooserState(app),
|
||||
NewConfigureRedmineQueryState(app),
|
||||
NewRemoveChatMemberState(app),
|
||||
NewHelpState(app),
|
||||
NewUnknownCommandState(app),
|
||||
@ -55,21 +49,51 @@ func PopulateStates(app iface.App) {
|
||||
errorState = NewErrorState(app.Log())
|
||||
}
|
||||
|
||||
func wizardPreHandle(w *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
func wizardRouter(w *Wizard, mc fsm.MachineControlsWithState[Wizard]) {
|
||||
if w.Data.Message != nil {
|
||||
if w.UserID == 0 {
|
||||
w.UserID = w.Data.Message.From.ID
|
||||
}
|
||||
if w.ChatID == 0 {
|
||||
w.ChatID = w.Data.Message.Chat.ID
|
||||
}
|
||||
if w.User == nil {
|
||||
user, err := appPointer.DB().ForUser().ByTelegramID(w.Data.Message.From.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil && user.ID > 0 {
|
||||
w.User = user
|
||||
}
|
||||
}
|
||||
if w.Chat == nil {
|
||||
chat, err := appPointer.DB().ForChat().ByTelegramID(w.Data.Message.Chat.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if chat != nil && chat.ID > 0 {
|
||||
w.Chat = chat
|
||||
}
|
||||
}
|
||||
if w.TGChat.ID == 0 {
|
||||
w.TGChat = w.Data.Message.Chat
|
||||
}
|
||||
|
||||
switch {
|
||||
case util.MatchCommand("start", w.Data.Message):
|
||||
mc.Move(RegisterStateID, w)
|
||||
_ = mc.Move(RegisterStateID, w)
|
||||
case util.MatchCommand("help", w.Data.Message):
|
||||
mc.Move(HelpStateID, w)
|
||||
_ = mc.Move(HelpStateID, w)
|
||||
case util.HasCommand(w.Data.Message):
|
||||
mc.Move(UnknownCommandStateID, w)
|
||||
_ = mc.Move(UnknownCommandStateID, w)
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if w.Data.MyChatMember != nil {
|
||||
mc.Move(WaitingForMemberWebhookStateID, w)
|
||||
_ = mc.Move(WaitingForMemberWebhookStateID, w)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ func NewKeyboardChooserState(app iface.App) fsm.IState[Wizard] {
|
||||
return &KeyboardChooserState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *KeyboardChooserState) Enter(pl *Wizard, _ fsm.MachineControls[*Wizard]) error {
|
||||
func (s *KeyboardChooserState) Enter(pl *Wizard, _ fsm.MachineControls[Wizard]) error {
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(pl.User.ChatID),
|
||||
Text: s.Localizer().Template("choose_keyboard", map[string]interface{}{"Name": pl.TGChat.Title}),
|
||||
@ -47,8 +47,43 @@ func (s *KeyboardChooserState) Enter(pl *Wizard, _ fsm.MachineControls[*Wizard])
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *KeyboardChooserState) Handle(pl *Wizard, _ fsm.MachineControls[*Wizard]) {
|
||||
// todo: implement this using func (h *CallbackQueryHandler) handleChooseKeyboard(pl util.Payload, msgID int, user *model.User) error
|
||||
func (s *KeyboardChooserState) Handle(pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
cr := s.App.DB().ForChat()
|
||||
result := pl.KeyClick.KeyboardChoice()
|
||||
if pl.KeyClick.User != pl.User.TelegramID {
|
||||
return
|
||||
}
|
||||
chat, err := cr.ByTelegramID(pl.KeyClick.Chat)
|
||||
if err != nil {
|
||||
s.LogError(err)
|
||||
return
|
||||
}
|
||||
if chat == nil || chat.ID == 0 {
|
||||
return
|
||||
}
|
||||
chat.KeyboardType = model.KeyboardType(result.Type)
|
||||
if err := cr.Save(chat); err != nil {
|
||||
s.LogError(err)
|
||||
return
|
||||
}
|
||||
|
||||
kbTypeName := "standard_vote_keyboard"
|
||||
if model.KeyboardType(result.Type) == model.StoryPointsKeyboard {
|
||||
kbTypeName = "sp_vote_keyboard"
|
||||
}
|
||||
loc := s.Localizer(pl.User.Language)
|
||||
_, err = s.App.TG().EditMessageText(&telego.EditMessageTextParams{
|
||||
ChatID: tu.ID(pl.User.ChatID),
|
||||
MessageID: pl.Data.CallbackQuery.Message.GetMessageID(),
|
||||
Text: loc.Template("chosen_keyboard", map[string]interface{}{"Name": loc.Message(kbTypeName)}),
|
||||
})
|
||||
s.LogError(err)
|
||||
|
||||
if !s.Payload.IsMaster {
|
||||
mc.Reset()
|
||||
return
|
||||
}
|
||||
s.Move(mc, ConfigureRedmineQueryStateID, pl)
|
||||
}
|
||||
|
||||
func (s *KeyboardChooserState) ID() fsm.StateID {
|
||||
|
@ -18,14 +18,14 @@ func NewRegisterState(app iface.App) fsm.IState[Wizard] {
|
||||
return &RegisterState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *RegisterState) Enter(pl *Wizard, _ fsm.MachineControls[*Wizard]) error {
|
||||
func (s *RegisterState) Enter(pl *Wizard, _ fsm.MachineControls[Wizard]) error {
|
||||
if pl.Data.Message != nil && pl.Data.Message.Chat.Type != telego.ChatTypePrivate {
|
||||
return fsm.ErrPreventTransition
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RegisterState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
func (s *RegisterState) Handle(pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
loc := s.Localizer(pl.Data.Message.From.LanguageCode)
|
||||
userRepo := s.App.DB().ForUser()
|
||||
user, err := userRepo.ByTelegramID(pl.Data.Message.From.ID)
|
||||
@ -70,6 +70,9 @@ func (s *RegisterState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
}
|
||||
|
||||
func (s *RegisterState) Exit(pl *Wizard) {
|
||||
if pl.Data.Message == nil {
|
||||
return
|
||||
}
|
||||
loc := s.Localizer(pl.Data.Message.From.LanguageCode)
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(pl.Data.Message.Chat.ID),
|
||||
|
@ -17,22 +17,26 @@ func NewRemoveChatMemberState(app iface.App) fsm.IState[Wizard] {
|
||||
return &RemoveChatMemberState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *RemoveChatMemberState) Enter(pl *Wizard, mc fsm.MachineControls[*Wizard]) error {
|
||||
func (s *RemoveChatMemberState) Enter(pl *Wizard, mc fsm.MachineControls[Wizard]) error {
|
||||
if pl.Data.MyChatMember == nil {
|
||||
s.Move(mc, WaitingForMemberWebhookStateID, pl)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoveChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
cr := s.App.DB().ForChat()
|
||||
chat, err := cr.ByTelegramID(pl.Data.MyChatMember.Chat.ID)
|
||||
if err != nil {
|
||||
s.LogError(err)
|
||||
return
|
||||
func (s *RemoveChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
if s.Payload.Chat == nil {
|
||||
chat, err := s.App.DB().ForChat().ByTelegramID(s.Payload.Data.MyChatMember.Chat.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if chat != nil && chat.ID > 0 {
|
||||
s.Payload.Chat = chat
|
||||
}
|
||||
}
|
||||
if chat != nil && chat.ID > 0 {
|
||||
user, _ := s.App.DB().ForUser().ByID(chat.UserID)
|
||||
if s.Payload.Chat != nil && s.Payload.Chat.ID > 0 {
|
||||
user, _ := s.App.DB().ForUser().ByID(s.Payload.Chat.UserID)
|
||||
if user != nil && user.ID > 0 && user.ChatID > 0 {
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(user.ChatID),
|
||||
@ -42,7 +46,7 @@ func (s *RemoveChatMemberState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizar
|
||||
})
|
||||
s.LogError(err)
|
||||
}
|
||||
s.LogError(cr.Delete(chat))
|
||||
s.LogError(s.App.DB().ForChat().Delete(s.Payload.Chat))
|
||||
}
|
||||
s.Move(mc, WaitingForMemberWebhookStateID, pl)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func NewUnknownCommandState(app iface.App) fsm.IState[Wizard] {
|
||||
return &UnknownCommandState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *UnknownCommandState) Enter(pl *Wizard, _ fsm.MachineControls[*Wizard]) error {
|
||||
func (s *UnknownCommandState) Enter(pl *Wizard, _ fsm.MachineControls[Wizard]) error {
|
||||
_, err := s.App.TG().SendMessage(&telego.SendMessageParams{
|
||||
ChatID: tu.ID(s.Payload.Data.Message.Chat.ID),
|
||||
Text: s.Localizer(s.Payload.Data.Message.From.LanguageCode).Message("unknown_command"),
|
||||
|
@ -17,22 +17,33 @@ func NewWaitingForMemberWebhookState(app iface.App) fsm.IState[Wizard] {
|
||||
return &WaitingForMemberWebhookState{newBase(app)}
|
||||
}
|
||||
|
||||
func (s *WaitingForMemberWebhookState) Handle(pl *Wizard, mc fsm.MachineControls[*Wizard]) {
|
||||
func (s *WaitingForMemberWebhookState) Enter(pl *Wizard, mc fsm.MachineControls[Wizard]) error {
|
||||
cm := pl.Data.MyChatMember
|
||||
if cm == nil {
|
||||
return nil
|
||||
}
|
||||
if !cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == s.App.TGProfile().ID &&
|
||||
cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == s.App.TGProfile().ID {
|
||||
s.Move(mc, AddChatMemberStateID, pl)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if cm.OldChatMember.MemberIsMember() && cm.OldChatMember.MemberUser().ID == s.App.TGProfile().ID &&
|
||||
!cm.NewChatMember.MemberIsMember() && cm.NewChatMember.MemberUser().ID == s.App.TGProfile().ID {
|
||||
s.Move(mc, RemoveChatMemberStateID, pl)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
s.LogError(s.App.TG().LeaveChat(&telego.LeaveChatParams{
|
||||
ChatID: tu.ID(pl.Data.MyChatMember.Chat.ID),
|
||||
}))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WaitingForMemberWebhookState) Handle(pl *Wizard, mc fsm.MachineControls[Wizard]) {
|
||||
// By using this we can handle both Move from Register state and Move from waiting for webhook state.
|
||||
// The first one doesn't have data yet and it will be handled by Handle, the second one has data and will be
|
||||
// handled by Enter.
|
||||
_ = s.Enter(pl, mc)
|
||||
}
|
||||
|
||||
func (s *WaitingForMemberWebhookState) ID() fsm.StateID {
|
||||
|
@ -1,11 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/group"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsmwizard"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/iface"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/store"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/util"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/wizard"
|
||||
"github.com/mymmrac/telego"
|
||||
)
|
||||
|
||||
@ -22,22 +19,30 @@ func (h *MessageHandler) Handle(wh telego.Update) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if util.MatchCommand("start", wh.Message) {
|
||||
return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
return fsmwizard.Get(wh.Message.From.ID).Handle(func(w *fsmwizard.Wizard) *fsmwizard.Wizard {
|
||||
if w == nil {
|
||||
return &fsmwizard.Wizard{Data: wh}
|
||||
}
|
||||
w.Data = wh
|
||||
return w
|
||||
})
|
||||
|
||||
if util.MatchCommand("poll", wh.Message) {
|
||||
return group.NewPoll(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
setup, found := store.RedmineSetups.Get(wh.Message.Chat.ID)
|
||||
if found {
|
||||
return wizard.NewRedmineSetup(h.App, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh)
|
||||
}
|
||||
|
||||
if util.MatchCommand("help", wh.Message) {
|
||||
return wizard.NewHelpCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
||||
return wizard.NewUnknownCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
// if util.MatchCommand("start", wh.Message) {
|
||||
// return wizard.NewRegister(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
// }
|
||||
//
|
||||
// if util.MatchCommand("poll", wh.Message) {
|
||||
// return group.NewPoll(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
// }
|
||||
//
|
||||
// setup, found := store.RedmineSetups.Get(wh.Message.Chat.ID)
|
||||
// if found {
|
||||
// return wizard.NewRedmineSetup(h.App, wh.Message.From.ID, wh.Message.Chat.ID, setup).Handle(wh)
|
||||
// }
|
||||
//
|
||||
// if util.MatchCommand("help", wh.Message) {
|
||||
// return wizard.NewHelpCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
// }
|
||||
//
|
||||
// return wizard.NewUnknownCommand(h.App, wh.Message.From.ID, wh.Message.Chat.ID).Handle(wh)
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package fsm
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -14,50 +14,49 @@ var (
|
||||
ErrStateDoesNotExist = errors.New("state does not exist")
|
||||
)
|
||||
|
||||
// MachineHandleInput should be provided to IMachine's Handle method. This function can do two very useful things:
|
||||
// - It can modify Machine's payload with input data.
|
||||
// MachineStateRouter should be provided to IMachine. This function can do two very useful things:
|
||||
// - It can modify Machine's payload.
|
||||
// - It can act as a router by changing Machine's state via provided controls.
|
||||
type MachineHandleInput[T any] func(*T, MachineControls[*T])
|
||||
type MachineStateRouter[T any] func(*T, MachineControlsWithState[T])
|
||||
|
||||
// MachineStateProvider provided to every Handle call. It can be used to set the initial state of the machine or to
|
||||
// update existing machine state.
|
||||
type MachineStateProvider[T any] func(*T) *T
|
||||
|
||||
// IMachine is a Machine contract. The Machine should be able to do the following:
|
||||
// - Move to another state (usually called by the IState itself).
|
||||
// - Handle the state input.
|
||||
// - Reset the machine.
|
||||
type IMachine[T any] interface {
|
||||
MachineControls[*T]
|
||||
MachineControlsWithState[T]
|
||||
// Handle the state input. Handle func will accept the current payload and modify it based on user input.
|
||||
Handle() error
|
||||
Handle(MachineStateProvider[T]) error
|
||||
// Reset the machine to its initial state.
|
||||
Reset()
|
||||
}
|
||||
|
||||
// Machine is a finite-state machine implementation.
|
||||
type Machine[T any] struct {
|
||||
lock sync.Mutex
|
||||
payload *T
|
||||
preHandle MachineHandleInput[T]
|
||||
state StateID
|
||||
initialState StateID
|
||||
initialPayload T
|
||||
states map[StateID]IState[T]
|
||||
errHandler ErrorState[T]
|
||||
payload *T
|
||||
stateRouter MachineStateRouter[T]
|
||||
state StateID
|
||||
initialState StateID
|
||||
states *types.Map[StateID, IState[T]]
|
||||
errHandler ErrorState[T]
|
||||
}
|
||||
|
||||
// New machine.
|
||||
func New[T any](initialState StateID, initialPayload T, preHandle MachineHandleInput[T], states []IState[T], errHandler ErrorState[T]) IMachine[T] {
|
||||
stateMap := make(map[StateID]IState[T], len(states))
|
||||
func New[T any](initialState StateID, router MachineStateRouter[T], states []IState[T], errHandler ErrorState[T]) IMachine[T] {
|
||||
stateMap := types.NewMap[StateID, IState[T]]()
|
||||
for _, state := range states {
|
||||
stateMap[state.ID()] = state
|
||||
stateMap.Set(state.ID(), state)
|
||||
}
|
||||
pl := initialPayload
|
||||
return &Machine[T]{
|
||||
state: initialState,
|
||||
payload: &pl,
|
||||
preHandle: preHandle,
|
||||
initialState: initialState,
|
||||
initialPayload: initialPayload,
|
||||
states: stateMap,
|
||||
errHandler: errHandler,
|
||||
state: initialState,
|
||||
stateRouter: router,
|
||||
initialState: initialState,
|
||||
states: stateMap,
|
||||
errHandler: errHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,8 +74,6 @@ func (m *Machine[T]) Move(id StateID, payload *T) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer m.lock.Unlock()
|
||||
m.lock.Lock()
|
||||
if cur != nil {
|
||||
cur.Exit(payload)
|
||||
}
|
||||
@ -93,11 +90,12 @@ func (m *Machine[T]) Move(id StateID, payload *T) error {
|
||||
}
|
||||
|
||||
// Handle the input.
|
||||
func (m *Machine[T]) Handle() error {
|
||||
defer m.lock.Unlock()
|
||||
m.lock.Lock()
|
||||
if m.preHandle != nil {
|
||||
m.preHandle(m.payload, m)
|
||||
func (m *Machine[T]) Handle(provider MachineStateProvider[T]) error {
|
||||
if provider != nil {
|
||||
m.payload = provider(m.payload)
|
||||
}
|
||||
if m.stateRouter != nil {
|
||||
m.stateRouter(m.payload, m)
|
||||
}
|
||||
st, err := m.loadState(m.state, m.payload)
|
||||
if st == nil || err != nil {
|
||||
@ -107,10 +105,14 @@ func (m *Machine[T]) Handle() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// State of the Machine.
|
||||
func (m *Machine[T]) State() *T {
|
||||
return m.payload
|
||||
}
|
||||
|
||||
// Reset the machine.
|
||||
func (m *Machine[T]) Reset() {
|
||||
pl := m.initialPayload
|
||||
m.payload = &pl
|
||||
m.payload = nil
|
||||
m.state = m.initialState
|
||||
}
|
||||
|
||||
@ -118,7 +120,7 @@ func (m *Machine[T]) loadState(id StateID, payload *T) (IState[T], error) {
|
||||
if id == NilStateID {
|
||||
return nil, nil
|
||||
}
|
||||
st, ok := m.states[id]
|
||||
st, ok := m.states.Get(id)
|
||||
if !ok {
|
||||
return nil, m.fatalError(fmt.Errorf("%w: %s", ErrStateDoesNotExist, id), id, payload)
|
||||
}
|
||||
|
@ -9,10 +9,22 @@ const NilStateID = StateID("")
|
||||
// MachineControls is a fragment of IMachine implementation. This one can Move between the states or Reset the machine.
|
||||
// It may fail with an error which should be handled by the IState implementation.
|
||||
type MachineControls[T any] interface {
|
||||
Move(StateID, T) error
|
||||
Move(StateID, *T) error
|
||||
Reset()
|
||||
}
|
||||
|
||||
// MachineState returns machine state. This state should be immutable.
|
||||
type MachineState[T any] interface {
|
||||
// State returns underlying state. This state SHOULD NOT be modified since there is no locking performed.
|
||||
State() *T
|
||||
}
|
||||
|
||||
// MachineControlsWithState is self-explanatory.
|
||||
type MachineControlsWithState[T any] interface {
|
||||
MachineControls[T]
|
||||
MachineState[T]
|
||||
}
|
||||
|
||||
// IState is a State interface. This contract enforces that any state implementation should have three methods:
|
||||
// Enter, Handle and Exit. The first one is called right after the machine has moved to the state, the second one is
|
||||
// called when handling some state input, the last one is called right before leaving to a next state (which happens
|
||||
@ -23,11 +35,11 @@ type IState[T any] interface {
|
||||
// Enter is a state enter callback. Can be used to perform some sort of input query or to move to another
|
||||
// state immediately. Also, if Enter fails the machine won't move to the state and MachineControls's Move will
|
||||
// return an error which was returned from Enter.
|
||||
Enter(*T, MachineControls[*T]) error
|
||||
Enter(*T, MachineControls[T]) error
|
||||
// Handle is called when receiving some sort of input response. This one's signature is nearly identical to Enter,
|
||||
// but it can wait for some sort of input (standard input? webhook) without locking inside the callback
|
||||
// while Enter cannot do that.
|
||||
Handle(*T, MachineControls[*T])
|
||||
Handle(*T, MachineControls[T])
|
||||
// Exit is called right before leaving the state. It can be used to modify state's payload (T) or for some other
|
||||
// miscellaneous tasks.
|
||||
// Note: calling Exit doesn't mean that the machine will really transition to the next state.
|
||||
@ -43,7 +55,7 @@ type IState[T any] interface {
|
||||
//
|
||||
// Machine without ErrorState will not do anything in case of fatal errors.
|
||||
type ErrorState[T any] interface {
|
||||
Handle(err error, current StateID, next StateID, payload *T, machine MachineControls[*T])
|
||||
Handle(err error, current StateID, next StateID, payload *T, machine MachineControls[T])
|
||||
}
|
||||
|
||||
// State is the Machine's state. This implementation doesn't do anything and only helps with the
|
||||
@ -58,12 +70,12 @@ func (s *State[T]) ID() StateID {
|
||||
}
|
||||
|
||||
// Enter here will immediately move the Machine to empty state.
|
||||
func (s *State[T]) Enter(payload *T, machine MachineControls[*T]) error {
|
||||
func (s *State[T]) Enter(payload *T, machine MachineControls[T]) error {
|
||||
return machine.Move(NilStateID, payload)
|
||||
}
|
||||
|
||||
// Handle here will immediately move the Machine to empty state.
|
||||
func (s *State[T]) Handle(payload *T, machine MachineControls[*T]) {
|
||||
func (s *State[T]) Handle(payload *T, machine MachineControls[T]) {
|
||||
_ = machine.Move(NilStateID, payload)
|
||||
}
|
||||
|
||||
|
27
pkg/types/map.go
Normal file
27
pkg/types/map.go
Normal file
@ -0,0 +1,27 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Map[K comparable, V any] struct {
|
||||
rw sync.RWMutex
|
||||
m map[K]V
|
||||
}
|
||||
|
||||
func NewMap[K comparable, V any]() *Map[K, V] {
|
||||
return &Map[K, V]{m: make(map[K]V)}
|
||||
}
|
||||
|
||||
func (t *Map[K, V]) Set(k K, v V) {
|
||||
defer t.rw.Unlock()
|
||||
t.rw.Lock()
|
||||
t.m[k] = v
|
||||
}
|
||||
|
||||
func (t *Map[K, V]) Get(k K) (val V, ok bool) {
|
||||
defer t.rw.RUnlock()
|
||||
t.rw.RLock()
|
||||
val, ok = t.m[k]
|
||||
return
|
||||
}
|
62
pkg/types/queue.go
Normal file
62
pkg/types/queue.go
Normal file
@ -0,0 +1,62 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type node[T any] struct {
|
||||
val atomic.Pointer[T]
|
||||
next atomic.Pointer[node[T]]
|
||||
}
|
||||
|
||||
type Queue[T any] interface {
|
||||
Enqueue(T)
|
||||
Dequeue() T
|
||||
Len() uint64
|
||||
}
|
||||
|
||||
type queue[T any] struct {
|
||||
head, tail *node[T]
|
||||
length atomic.Uint64
|
||||
}
|
||||
|
||||
func NewQueue[T any]() Queue[T] {
|
||||
n := node[T]{}
|
||||
return &queue[T]{head: &n, tail: &n}
|
||||
}
|
||||
|
||||
func (q *queue[T]) Enqueue(v T) {
|
||||
n := node[T]{val: atomic.Pointer[T]{}}
|
||||
n.val.Store(&v)
|
||||
for {
|
||||
if q.tail.next.CompareAndSwap(nil, &n) {
|
||||
atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&q.tail)), unsafe.Pointer(&n))
|
||||
q.length.Add(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *queue[T]) Dequeue() T {
|
||||
headAddr := (*unsafe.Pointer)(unsafe.Pointer(&q.head))
|
||||
for {
|
||||
var result T
|
||||
head := atomic.LoadPointer(headAddr)
|
||||
n := q.head.next.Load()
|
||||
if n == nil {
|
||||
return result
|
||||
}
|
||||
if atomic.CompareAndSwapPointer(headAddr, head, unsafe.Pointer(n)) {
|
||||
q.length.Add(^uint64(0)) // Переполнение намеренное, это отнимает единицу от счетчика.
|
||||
if r := n.val.Load(); r != nil {
|
||||
return *r
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (q *queue[T]) Len() uint64 {
|
||||
return q.length.Load()
|
||||
}
|
19
pkg/types/queue_test.go
Normal file
19
pkg/types/queue_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestQueue(t *testing.T) {
|
||||
q := NewQueue[uint8]()
|
||||
assert.Equal(t, uint64(0), q.Len())
|
||||
assert.Equal(t, uint8(0), q.Dequeue())
|
||||
|
||||
q.Enqueue(1)
|
||||
q.Enqueue(2)
|
||||
assert.Equal(t, uint64(2), q.Len())
|
||||
assert.Equal(t, uint8(1), q.Dequeue())
|
||||
assert.Equal(t, uint8(2), q.Dequeue())
|
||||
assert.Equal(t, uint8(0), q.Dequeue())
|
||||
}
|
63
pkg/types/ttl_map.go
Normal file
63
pkg/types/ttl_map.go
Normal file
@ -0,0 +1,63 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TTLMap[K comparable, V any] struct {
|
||||
rw sync.RWMutex
|
||||
m map[K]entry[V]
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
type entry[V any] struct {
|
||||
value V
|
||||
expireAt *time.Time
|
||||
}
|
||||
|
||||
func (e *entry[V]) expired() bool {
|
||||
if e.expireAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(*e.expireAt)
|
||||
}
|
||||
|
||||
func NewTTLMap[K comparable, V any](ttl time.Duration) *TTLMap[K, V] {
|
||||
return &TTLMap[K, V]{m: make(map[K]entry[V]), ttl: ttl}
|
||||
}
|
||||
|
||||
func (t *TTLMap[K, V]) Set(k K, v V) {
|
||||
t.SetWithTTL(k, v, t.ttl)
|
||||
}
|
||||
|
||||
func (t *TTLMap[K, V]) SetWithTTL(k K, v V, ttl time.Duration) {
|
||||
defer t.rw.Unlock()
|
||||
t.rw.Lock()
|
||||
var expireAt *time.Time
|
||||
if ttl > 0 {
|
||||
at := time.Now().Add(ttl)
|
||||
expireAt = &at
|
||||
}
|
||||
t.m[k] = entry[V]{value: v, expireAt: expireAt}
|
||||
}
|
||||
|
||||
func (t *TTLMap[K, V]) Get(k K) (val V, ok bool) {
|
||||
defer t.rw.RUnlock()
|
||||
t.rw.RLock()
|
||||
v, ok := t.m[k]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v.expired() {
|
||||
defer func() {
|
||||
t.rw.RUnlock()
|
||||
t.rw.Lock()
|
||||
delete(t.m, k)
|
||||
t.rw.Unlock()
|
||||
t.rw.RLock()
|
||||
}()
|
||||
return
|
||||
}
|
||||
return v.value, true
|
||||
}
|
Loading…
Reference in New Issue
Block a user