implement fsm, fix translations
This commit is contained in:
parent
e6949e89ed
commit
0e2cc70a09
3
go.mod
3
go.mod
@ -9,6 +9,7 @@ require (
|
|||||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/maypok86/otter v1.2.1
|
||||||
github.com/mymmrac/telego v0.29.2
|
github.com/mymmrac/telego v0.29.2
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
@ -25,7 +26,9 @@ require (
|
|||||||
github.com/bytedance/sonic v1.11.3 // indirect
|
github.com/bytedance/sonic v1.11.3 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
|
github.com/dolthub/maphash v0.1.0 // indirect
|
||||||
github.com/fasthttp/router v1.5.0 // indirect
|
github.com/fasthttp/router v1.5.0 // indirect
|
||||||
|
github.com/gammazero/deque v0.2.1 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/grbit/go-json v0.11.0 // indirect
|
github.com/grbit/go-json v0.11.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
6
go.sum
6
go.sum
@ -39,8 +39,12 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
|||||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
|
github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
|
||||||
|
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
|
||||||
github.com/fasthttp/router v1.5.0 h1:3Qbbo27HAPzwbpRzgiV5V9+2faPkPt3eNuRaDV6LYDA=
|
github.com/fasthttp/router v1.5.0 h1:3Qbbo27HAPzwbpRzgiV5V9+2faPkPt3eNuRaDV6LYDA=
|
||||||
github.com/fasthttp/router v1.5.0/go.mod h1:FddcKNXFZg1imHcy+uKB0oo/o6yE9zD3wNguqlhWDak=
|
github.com/fasthttp/router v1.5.0/go.mod h1:FddcKNXFZg1imHcy+uKB0oo/o6yE9zD3wNguqlhWDak=
|
||||||
|
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
|
||||||
|
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
|
||||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
@ -96,6 +100,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
|||||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/maypok86/otter v1.2.1 h1:xyvMW+t0vE1sKt/++GTkznLitEl7D/msqXkAbLwiC1M=
|
||||||
|
github.com/maypok86/otter v1.2.1/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
|
||||||
github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU=
|
github.com/microsoft/go-mssqldb v1.0.0 h1:k2p2uuG8T5T/7Hp7/e3vMGTnnR0sU4h8d1CcC71iLHU=
|
||||||
github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4=
|
github.com/microsoft/go-mssqldb v1.0.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4=
|
||||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||||
|
22
internal/handler/fsm/fsmcontract/context.go
Normal file
22
internal/handler/fsm/fsmcontract/context.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context interface {
|
||||||
|
App() App
|
||||||
|
Data() map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type App interface {
|
||||||
|
Log() *zap.SugaredLogger
|
||||||
|
TG() *telego.Bot
|
||||||
|
Conf() *config.Config
|
||||||
|
DB() *db.Repositories
|
||||||
|
Localizer(string) locale.Localizer
|
||||||
|
}
|
7
internal/handler/fsm/fsmcontract/machine.go
Normal file
7
internal/handler/fsm/fsmcontract/machine.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package fsmcontract
|
||||||
|
|
||||||
|
type Machine interface {
|
||||||
|
Reset()
|
||||||
|
Handle()
|
||||||
|
HandleError(error)
|
||||||
|
}
|
5
internal/handler/fsm/fsmcontract/state.go
Normal file
5
internal/handler/fsm/fsmcontract/state.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package fsmcontract
|
||||||
|
|
||||||
|
type State interface {
|
||||||
|
Handle(ctx Context) (State, Context, error)
|
||||||
|
}
|
25
internal/handler/fsm/fsmcore/atomic_value.go
Normal file
25
internal/handler/fsm/fsmcore/atomic_value.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package fsmcore
|
||||||
|
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
|
type AtomicValue[T any] struct {
|
||||||
|
ptr atomic.Pointer[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAtomicValue[T any](val T) *AtomicValue[T] {
|
||||||
|
value := &AtomicValue[T]{}
|
||||||
|
value.ptr.Store(&val)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AtomicValue[T]) Load() (result T) {
|
||||||
|
val := v.ptr.Load()
|
||||||
|
if val == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *AtomicValue[T]) Store(value T) {
|
||||||
|
v.ptr.Store(&value)
|
||||||
|
}
|
57
internal/handler/fsm/fsmcore/machine.go
Normal file
57
internal/handler/fsm/fsmcore/machine.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package fsmcore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
MachineErrorHandler func(fsmcontract.Machine, fsmcontract.Context, error)
|
||||||
|
NewContextFunc func() fsmcontract.Context
|
||||||
|
)
|
||||||
|
|
||||||
|
type Machine struct {
|
||||||
|
ctx *AtomicValue[fsmcontract.Context]
|
||||||
|
entryState fsmcontract.State
|
||||||
|
state *AtomicValue[fsmcontract.State]
|
||||||
|
newContextFunc NewContextFunc
|
||||||
|
errHandler MachineErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMachine(ctx fsmcontract.Context, entryState fsmcontract.State,
|
||||||
|
newContextFunc NewContextFunc, errHandler MachineErrorHandler) *Machine {
|
||||||
|
return &Machine{
|
||||||
|
ctx: NewAtomicValue[fsmcontract.Context](ctx),
|
||||||
|
entryState: entryState,
|
||||||
|
state: NewAtomicValue[fsmcontract.State](entryState),
|
||||||
|
newContextFunc: newContextFunc,
|
||||||
|
errHandler: errHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Machine) Handle() {
|
||||||
|
ctx := m.ctx.Load()
|
||||||
|
state := m.state.Load()
|
||||||
|
next, newCtx, err := state.Handle(ctx)
|
||||||
|
if err != nil {
|
||||||
|
m.HandleError(err)
|
||||||
|
}
|
||||||
|
if next != state {
|
||||||
|
m.state.Store(next)
|
||||||
|
}
|
||||||
|
m.ctx.Store(newCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Machine) Reset() {
|
||||||
|
var ctx fsmcontract.Context
|
||||||
|
if m.newContextFunc != nil {
|
||||||
|
ctx = m.newContextFunc()
|
||||||
|
}
|
||||||
|
m.ctx.Store(ctx)
|
||||||
|
m.state.Store(m.entryState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Machine) HandleError(err error) {
|
||||||
|
if m.errHandler != nil {
|
||||||
|
m.errHandler(m, m.ctx.Load(), err)
|
||||||
|
}
|
||||||
|
}
|
17
internal/handler/fsm/fsmcore/state.go
Normal file
17
internal/handler/fsm/fsmcore/state.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package fsmcore
|
||||||
|
|
||||||
|
import "gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||||
|
|
||||||
|
type HandleFunc func(ctx fsmcontract.Context) (fsmcontract.State, fsmcontract.Context, error)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
next HandleFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Handle(ctx fsmcontract.Context) (fsmcontract.State, fsmcontract.Context, error) {
|
||||||
|
return s.next(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState(next HandleFunc) fsmcontract.State {
|
||||||
|
return &State{next: next}
|
||||||
|
}
|
19
internal/handler/fsm/machine/dto/context.go
Normal file
19
internal/handler/fsm/machine/dto/context.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||||
|
"github.com/mymmrac/telego"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Application fsmcontract.App
|
||||||
|
Update telego.Update
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) App() fsmcontract.App {
|
||||||
|
return c.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Data() map[string]interface{} {
|
||||||
|
return map[string]interface{}{"update": c.Update}
|
||||||
|
}
|
119
internal/handler/fsm/machine/dto/payload.go
Normal file
119
internal/handler/fsm/machine/dto/payload.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PayloadAction uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
PayloadActionNext = iota
|
||||||
|
PayloadActionPrevious
|
||||||
|
PayloadActionAddMember
|
||||||
|
PayloadActionRemoveMember
|
||||||
|
PayloadActionYesNo
|
||||||
|
PayloadActionVote
|
||||||
|
)
|
||||||
|
|
||||||
|
type QuestionType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
QuestionTypeChatMembers QuestionType = iota
|
||||||
|
QuestionTypeRedmine
|
||||||
|
QuestionTypeRedmineHours
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
User uint64 `json:"u,omitempty"`
|
||||||
|
Chat uint64 `json:"c,omitempty"`
|
||||||
|
Action PayloadAction `json:"a"`
|
||||||
|
Data json.RawMessage `json:"d,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
ID uint64
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vote struct {
|
||||||
|
Vote float32 `json:"v"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Answer struct {
|
||||||
|
Type QuestionType `json:"t"`
|
||||||
|
Result bool `json:"r"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNextPageMembersPayload(userID, chatID uint64) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionNext,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPrevPageMembersPayload(userID, chatID uint64) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionPrevious,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAddMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionAddMember,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Member{ID: memberID, Name: memberName}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRemoveMemberPayload(userID, chatID, memberID uint64, memberName string) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionRemoveMember,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Member{ID: memberID, Name: memberName}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVotePayload(chatID uint64, vote float32) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionVote,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Vote{Vote: vote}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfirmMembersPayload(userID uint64, chatID uint64) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionYesNo,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Answer{Type: QuestionTypeChatMembers, Result: true}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedmineQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionYesNo,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Answer{Type: QuestionTypeRedmine, Result: isYes}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedmineHoursQuestionPayload(userID uint64, chatID uint64, isYes bool) *Payload {
|
||||||
|
return &Payload{
|
||||||
|
Action: PayloadActionYesNo,
|
||||||
|
User: userID,
|
||||||
|
Chat: chatID,
|
||||||
|
Data: marshal(Answer{Type: QuestionTypeRedmineHours, Result: isYes}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshal(val interface{}) json.RawMessage {
|
||||||
|
data, _ := json.Marshal(val)
|
||||||
|
return data
|
||||||
|
}
|
41
internal/handler/fsm/machine/polls.go
Normal file
41
internal/handler/fsm/machine/polls.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/machine/dto"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/maypok86/otter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberVote struct {
|
||||||
|
dto.Member
|
||||||
|
Vote float32
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollResult struct {
|
||||||
|
Max float32
|
||||||
|
Min float32
|
||||||
|
Avg float32
|
||||||
|
Halved float32
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollState struct {
|
||||||
|
Members []dto.Member
|
||||||
|
Votes []MemberVote
|
||||||
|
Result PollResult
|
||||||
|
}
|
||||||
|
|
||||||
|
var Polls otter.Cache[uint64, PollState]
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cache, err := otter.MustBuilder[uint64, PollState](10_000).
|
||||||
|
Cost(func(key uint64, value PollState) uint32 {
|
||||||
|
return 1
|
||||||
|
}).
|
||||||
|
WithTTL(time.Hour * 24 * 7).
|
||||||
|
Build()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
Polls = cache
|
||||||
|
}
|
63
internal/handler/fsm/machine/store.go
Normal file
63
internal/handler/fsm/machine/store.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package machine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler/fsm/fsmcontract"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
userMachines = newStore[uint64, fsmcontract.Machine]()
|
||||||
|
pollMachines = newStore[uint64, fsmcontract.Machine]()
|
||||||
|
)
|
||||||
|
|
||||||
|
type store[K comparable, V any] struct {
|
||||||
|
m map[K]V
|
||||||
|
l sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStore[K comparable, V any]() *store[K, V] {
|
||||||
|
return &store[K, V]{m: map[K]V{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store[K, V]) Store(k K, v V) {
|
||||||
|
defer s.l.Unlock()
|
||||||
|
s.l.Lock()
|
||||||
|
s.m[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *store[K, V]) Load(k K) (v V, ok bool) {
|
||||||
|
defer s.l.RUnlock()
|
||||||
|
s.l.RLock()
|
||||||
|
v, ok = s.m[k]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForUser(id uint64) fsmcontract.Machine {
|
||||||
|
val, ok := userMachines.Load(id)
|
||||||
|
if !ok {
|
||||||
|
machine := newUserMachine()
|
||||||
|
userMachines.Store(id, machine)
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForPoll(id uint64) fsmcontract.Machine {
|
||||||
|
val, ok := pollMachines.Load(id)
|
||||||
|
if !ok {
|
||||||
|
machine := newPollMachine()
|
||||||
|
pollMachines.Store(id, machine)
|
||||||
|
return machine
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUserMachine() fsmcontract.Machine {
|
||||||
|
// todo implement this
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPollMachine() fsmcontract.Machine {
|
||||||
|
// todo implement this
|
||||||
|
panic("not implemented yet")
|
||||||
|
}
|
131
internal/handler/keyboard.go
Normal file
131
internal/handler/keyboard.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "github.com/mymmrac/telego"
|
||||||
|
|
||||||
|
func StandardVoteKeyboard() *telego.InlineKeyboardMarkup {
|
||||||
|
return &telego.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "0.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
{
|
||||||
|
Text: "3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "13",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
{
|
||||||
|
Text: "20",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "40",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "100",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoryPointsVoteKeyboard() *telego.InlineKeyboardMarkup {
|
||||||
|
return &telego.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]telego.InlineKeyboardButton{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "0.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "1.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "2.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "3.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "4.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "5.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "6.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "7.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
Text: "8.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "9",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "9.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Text: "10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,19 @@
|
|||||||
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."
|
||||||
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."
|
||||||
choose_at_least_one: "You must choose at least one participant."
|
previous: "◀️ Back"
|
||||||
ask_for_redmine: "Configure integration with Redmine?"
|
next: "Next ▶️"
|
||||||
|
choose_keyboard: "⌨️ Choose a keyboard for poker. There are two types of keyboards available:\n\n• Standard - for regular poker, it uses Fibonacci numbers up to 13;\n• Story Points Poker Keyboard - a keyboard with buttons from 0.5 to 10 with step 0.5."
|
||||||
|
standard_vote_keyboard: "Standard poker keyboard"
|
||||||
|
sp_vote_keyboard: "Story Points poker keyboard"
|
||||||
|
choose_at_least_one: "❌ You must choose at least one participant."
|
||||||
|
ask_for_redmine: "📕 Configure integration with Redmine?"
|
||||||
please_send_redmine_url: "Please send the URL of your Redmine instance."
|
please_send_redmine_url: "Please send the URL of your Redmine instance."
|
||||||
please_send_redmine_key: "Please send your API key that will be used by the bot to interact with Redmine. The bot will perform the following actions:\n\n• retrieving task data;\n• writing the result of the poker in the task (if configured in subsequent steps).\n\nThe access key for the API can be found on the left part of the page by [this link](/my/account)."
|
please_send_redmine_key: "Please send your API key that will be used by the bot to interact with Redmine. The bot will perform the following actions:\n\n• retrieving task data;\n• writing the result of the poker in the task (if configured in subsequent steps).\n\nThe access key for the API can be found on the left part of the page by [this link](/my/account)."
|
||||||
invalid_redmine_credentials: "Failed to verify connection with Redmine. Will try setting up again?"
|
invalid_redmine_credentials: "⚠️ Failed to verify connection with Redmine. Will try setting up again?"
|
||||||
redmine_was_connected: "Great, Redmine connected! Now when starting a vote and passing it on to `/poll@vegapokerbot` in the chat, the bot will mention the task theme and generate a link to the task.\n\nThe bot can pass results of the vote directly into custom fields of issues in Redmine. The following data are passed:\n\n• the vote result;\n• the vote result converted to hours (assuming that the vote result is story points, conversion to hours is automatic).\n\nDo you want to send this data to Redmine at the end of the poker?"
|
redmine_was_connected: "✔️ Great, Redmine is now connected! Now when starting a vote and passing it on to `/poll@vegapokerbot` in the chat, the bot will mention the task theme and generate a link to the task.\n\nThe bot can pass results of the vote directly into custom fields of issues in Redmine. The following data are passed:\n\n• the vote result;\n• the vote result converted to hours (assuming that the vote result is story points, conversion to hours is automatic).\n\nDo you want to send this data to Redmine at the end of the poker?"
|
||||||
specify_result_field: "Specify the name of the field where the poker result will be written."
|
specify_result_field: "Specify the name of the field where the poker result will be written."
|
||||||
should_also_send_hours: "Do you want the poker result to be converted to hours and also sent to Redmine? This is useful if you treat the vote result as story points. In that case the result will be multiplied by 8 before sending it to Redmine. Example: vote result 1.5 means that we will send to Redmine 1.5 * 8 = 12."
|
should_also_send_hours: "Do you want the poker result to be converted to hours and also sent to Redmine? This is useful if you treat the vote result as story points. In that case the result will be multiplied by 8 before sending it to Redmine. Example: vote result 1.5 means that we will send to Redmine 1.5 * 8 = 12."
|
||||||
specify_second_result_field: "Specify the name of the field where the poker result in hours will be writen."
|
specify_second_result_field: "Specify the name of the field where the poker result in hours will be writen."
|
||||||
setup_done: "Done, now your chat is connected to the bot!\nUse the command`/poll@vegapokerbot` in the connected chat to start a vote."
|
setup_done: "✔️ Done, now your chat is connected to the bot!\nUse the command`/poll@vegapokerbot` in the connected chat to start a vote."
|
||||||
yes: "Yes"
|
yes: "✔️ Yes"
|
||||||
no: "No"
|
no: "✖️ No"
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
welcome: "Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
welcome: "👋 Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
||||||
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
|
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
|
||||||
choose_at_least_one: "Вы должны выбрать хотя бы одного участника."
|
previous: "◀️ Назад"
|
||||||
ask_for_redmine: "Настроить интеграцию с Redmine?"
|
next: "Вперед ▶️"
|
||||||
|
choose_keyboard: "⌨️ Выберите клавиатуру для голосований в чате. Доступны два вида клавиатуры:\n\n• Стандартная - для обычного покера, использует Фибоначчи до 13;\n• Story Points клавиатура - клавиатура от 0.5 до 10 с шагом 0.5."
|
||||||
|
standard_vote_keyboard: "Стандартная poker-клавиатура"
|
||||||
|
sp_vote_keyboard: "Story Points poker-клавиатура"
|
||||||
|
choose_at_least_one: "❌ Вы должны выбрать хотя бы одного участника."
|
||||||
|
ask_for_redmine: "📕 Настроить интеграцию с Redmine?"
|
||||||
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
|
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
|
||||||
please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке](/my/account)."
|
please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке](/my/account)."
|
||||||
invalid_redmine_credentials: "Не удалось проверить связь с Redmine. Будете пробовать настроить заново?"
|
invalid_redmine_credentials: "⚠️ Не удалось проверить связь с Redmine. Будете пробовать настроить заново?"
|
||||||
redmine_was_connected: "Отлично, Redmine подключен! Теперь при начале голосования и передаче в команду `/poll@vegapokerbot` номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в кастомные поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая).\n\nБудете настраивать передачу этих данных?"
|
redmine_was_connected: "✔️ Отлично, теперь Redmine подключен! Теперь при начале голосования и передаче в команду `/poll@vegapokerbot` номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в кастомные поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая).\n\nБудете настраивать передачу этих данных?"
|
||||||
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
||||||
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
||||||
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
||||||
setup_done: "Готово, чат подключен к боту!\nИспользуйте команду`/poll@vegapokerbot` в подключенном чате для запуска голосования."
|
setup_done: "✔️ Готово, чат подключен к боту!\nИспользуйте команду`/poll@vegapokerbot` в подключенном чате для запуска голосования."
|
||||||
yes: "Да"
|
yes: "✔️ Да"
|
||||||
no: "Нет"
|
no: "✖️ Нет"
|
||||||
|
Loading…
Reference in New Issue
Block a user