From 94d8c38dd54a9b10b2ee3ad09b25dbda0a0ad041 Mon Sep 17 00:00:00 2001 From: surdeus Date: Wed, 16 Aug 2023 17:25:56 +0300 Subject: [PATCH] Finally implemented JSON-ing of behaviours. --- cmd/json/main.go | 195 ++++++++++++++++++++++++--------------------- cmd/json/mkfile | 5 ++ cmd/test/main.go | 6 +- src/tx/action.go | 41 +++++++--- src/tx/beh.go | 12 +-- src/tx/bot.go | 47 ++++++----- src/tx/button.go | 4 +- src/tx/command.go | 4 +- src/tx/context.go | 5 +- src/tx/encoding.go | 37 +++++++++ src/tx/errors.go | 1 + src/tx/json.go | 34 ++++++++ src/tx/main.go | 5 +- src/tx/screen.go | 4 +- 14 files changed, 257 insertions(+), 143 deletions(-) create mode 100644 cmd/json/mkfile create mode 100644 src/tx/encoding.go create mode 100644 src/tx/json.go diff --git a/cmd/json/main.go b/cmd/json/main.go index 776f5f9..a5b3695 100644 --- a/cmd/json/main.go +++ b/cmd/json/main.go @@ -18,9 +18,18 @@ type UserData struct { Counter int } -type Code string +type Code struct { + Code string + Add int +} -func (c Code) Act(a *tx.A) { +func NewCode(code string) *Code { + return &Code{ + Code: code, + } +} + +func (c *Code) Act(a *tx.A) { var err error fmt.Println("In Act") e := env.NewEnv() @@ -32,52 +41,46 @@ func (c Code) Act(a *tx.A) { panic(err) } - _, err = vm.Execute(e, nil, string(c)) + _, err = vm.Execute(e, nil, c.Code) if err != nil { panic(err) } } -var startScreenButton = tx.NewButton("🏠 To the start screen"). - WithAction(Code(` +func main() { + tx.DefineAction("goscript", &Code{}) + + var startScreenButton = tx.NewButton("🏠 To the start screen"). + WithAction(NewCode(` a.ChangeScreen("start") `)) -var beh = tx.NewBehaviour(). - - // The function will be called every time - // the bot is started. - WithStart(Code(` - a.V = new(UserData) - a.ChangeScreen("start") - `)).WithKeyboards( - - // Increment/decrement keyboard. - tx.NewKeyboard("inc/dec").Row( - tx.NewButton("+").WithAction(Code(` + var ( + incDecKeyboard = tx.NewKeyboard("").Row( + tx.NewButton("+").WithAction(NewCode(` d = a.V d.Counter++ a.Sendf("%d", d.Counter) `)), - tx.NewButton("-").WithAction(Code(` + tx.NewButton("-").WithAction(NewCode(` d = a.V d.Counter-- a.Sendf("%d", d.Counter) `)), - ).Row( - startScreenButton, - ), + ).Row( + startScreenButton, + ) - // The navigational keyboard. - tx.NewKeyboard("nav").Row( - tx.NewButton("Inc/Dec").WithAction(Code(`a.ChangeScreen("inc/dec")`)), - ).Row( - tx.NewButton("Upper case").WithAction(Code(`a.ChangeScreen("upper-case")`)), - tx.NewButton("Lower case").WithAction(Code(`a.ChangeScreen("lower-case")`)), - ).Row( - tx.NewButton("Send location"). - WithSendLocation(true). - WithAction(Code(` + // The navigational keyboard. + navKeyboard = tx.NewKeyboard("").Row( + tx.NewButton("Inc/Dec").WithAction(NewCode(`a.ChangeScreen("inc/dec")`)), + ).Row( + tx.NewButton("Upper case").WithAction(NewCode(`a.ChangeScreen("upper-case")`)), + tx.NewButton("Lower case").WithAction(NewCode(`a.ChangeScreen("lower-case")`)), + ).Row( + tx.NewButton("Send location"). + WithSendLocation(true). + WithAction(NewCode(` err = nil if a.U.Message.Location != nil { l = a.U.Message.Location @@ -89,46 +92,53 @@ var beh = tx.NewBehaviour(). a.Send(err) } `)), - ), + ) - tx.NewKeyboard("istart").Row( - tx.NewButton("My Telegram"). - WithUrl("https://t.me/surdeus"), - ), + inlineKeyboard = tx.NewKeyboard("").Row( + tx.NewButton("My Telegram"). + WithUrl("https://t.me/surdeus"), + ) - // The keyboard to return to the start screen. - tx.NewKeyboard("nav-start").Row( - startScreenButton, - ), -).WithScreens( - tx.NewScreen("start"). - WithText( - "The bot started!"+ - " The bot is supposed to provide basic"+ - " understand of how the API works, so just"+ - " horse around a bit to guess everything out"+ - " by yourself!", - ).Keyboard("nav"). - IKeyboard("istart"), + // The keyboard to return to the start screen. + navToStartKeyboard = tx.NewKeyboard("nav-start").Row( + startScreenButton, + ) + ) + var beh = tx.NewBehaviour(). + // The function will be called every time + // the bot is started. + WithInit(NewCode(` + a.V = new(UserData) + `)). + WithScreens( + tx.NewScreen("start"). + WithText( + "The bot started!"+ + " The bot is supposed to provide basic"+ + " understand of how the API works, so just"+ + " horse around a bit to guess everything out"+ + " by yourself!", + ).WithKeyboard(navKeyboard). + WithIKeyboard(inlineKeyboard), - tx.NewScreen("inc/dec"). - WithText( - "The screen shows how "+ - "user separated data works "+ - "by saving the counter for each of users "+ - "separately. ", - ). - Keyboard("inc/dec"). - // The function will be called when reaching the screen. - WithAction(Code(` + tx.NewScreen("inc/dec"). + WithText( + "The screen shows how "+ + "user separated data works "+ + "by saving the counter for each of users "+ + "separately. ", + ). + WithKeyboard(incDecKeyboard). + // The function will be called when reaching the screen. + WithAction(NewCode(` d = a.V a.Sendf("Current counter value = %d", d.Counter) `)), - tx.NewScreen("upper-case"). - WithText("Type text and the bot will send you the upper case version to you"). - Keyboard("nav-start"). - WithAction(Code(` + tx.NewScreen("upper-case"). + WithText("Type text and the bot will send you the upper case version to you"). + WithKeyboard(navToStartKeyboard). + WithAction(NewCode(` strings = import("strings") for { msg, err = a.ReadTextMessage() @@ -145,10 +155,10 @@ var beh = tx.NewBehaviour(). } `)), - tx.NewScreen("lower-case"). - WithText("Type text and the bot will send you the lower case version"). - Keyboard("nav-start"). - WithAction(Code(` + tx.NewScreen("lower-case"). + WithText("Type text and the bot will send you the lower case version"). + WithKeyboard(navToStartKeyboard). + WithAction(NewCode(` strings = import("strings") for { msg, err = a.ReadTextMessage() @@ -164,42 +174,47 @@ var beh = tx.NewBehaviour(). } } `)), -).WithCommands( - tx.NewCommand("hello"). - Desc("sends the 'Hello, World!' message back"). - WithAction(Code(` - a.Send("Hello, World!") - `)), - tx.NewCommand("read"). - Desc("reads a string and sends it back"). - WithAction(Code(` - a.Send("Type some text:") - msg, err = a.ReadTextMessage() - if err != nil { - return - } - a.Sendf("You typed %q", msg) - `)), -) - -func main() { + ).WithCommands( + tx.NewCommand("start"). + Desc("start or restart the bot"). + WithAction(NewCode(` + a.ChangeScreen("start") + `)), + tx.NewCommand("hello"). + Desc("sends the 'Hello, World!' message back"). + WithAction(NewCode(` + a.Send("Hello, World!") + `)), + tx.NewCommand("read"). + Desc("reads a string and sends it back"). + WithAction(NewCode(` + a.Send("Type some text:") + msg, err = a.ReadTextMessage() + if err != nil { + return + } + a.Sendf("You typed %q", msg) + `)), + ) bts, err := json.MarshalIndent(beh, "", "\t") if err != nil { panic(err) } fmt.Printf("%s", bts) - /*jBeh := &tx.Behaviour{} + jBeh := &tx.Behaviour{} err = json.Unmarshal(bts, jBeh) if err != nil { panic(err) - }*/ + } - bot, err := tx.NewBot(os.Getenv("BOT_TOKEN"), beh, nil) + bot, err := tx.NewBot(os.Getenv("BOT_TOKEN")) if err != nil { panic(err) } + bot = bot.WithBehaviour(jBeh) + err = bot.Run() if err != nil { panic(err) diff --git a/cmd/json/mkfile b/cmd/json/mkfile new file mode 100644 index 0000000..96a8df2 --- /dev/null +++ b/cmd/json/mkfile @@ -0,0 +1,5 @@ +all: + go build + +run:V: + ./jsoned diff --git a/cmd/test/main.go b/cmd/test/main.go index db82e7c..d3f1c04 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -77,11 +77,11 @@ var ( ) var beh = tx.NewBehaviour(). - OnStartFunc(func(c *tx.A) { - // The function will be called every time - // the bot is started. + WithInitFunc(func(c *tx.A) { + // The session initialization. c.V = &UserData{} c.ChangeScreen("start") + }).WithScreens( tx.NewScreen("start"). WithText( diff --git a/src/tx/action.go b/src/tx/action.go index 2f21f3f..3d41543 100644 --- a/src/tx/action.go +++ b/src/tx/action.go @@ -1,17 +1,43 @@ package tx -//apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" +import ( + "reflect" +) +// Jsonable Action. +type action struct { + Type string + Action Action +} + +func newAction(a Action) *action { + typ, ok := actionMapByReflect[reflect.TypeOf(a)] + if !ok { + panic(ActionNotDefinedErr) + } + + return &action{ + Type: typ, + Action: a, + } +} + +func (a *action) Act(arg *A) { + if a.Action != nil { + a.Action.Act(arg) + } +} + +// Customized actions for the bot. type Action interface { Act(*Arg) } +// Customized actions for the type GroupAction interface { Act(*GroupArg) } -// Customized actions for the bot. - type ActionFunc func(*Arg) func (af ActionFunc) Act(a *Arg) { @@ -99,12 +125,3 @@ type ChannelAction struct { type JsonTyper interface { JsonType() string } - -type JsonAction struct { - Type string - Action Action -} - -func (ja JsonAction) UnmarshalJSON(bts []byte, ptr any) error { - return nil -} diff --git a/src/tx/beh.go b/src/tx/beh.go index 08fc683..2e95ecc 100644 --- a/src/tx/beh.go +++ b/src/tx/beh.go @@ -5,7 +5,7 @@ package tx // The type describes behaviour for the bot in personal chats. type Behaviour struct { - Start Action + Init *action Screens ScreenMap Keyboards KeyboardMap Commands CommandMap @@ -20,15 +20,17 @@ func NewBehaviour() *Behaviour { } } -func (b *Behaviour) WithStart(a Action) *Behaviour { - b.Start = a +// The Action will be called on session creation, +// not when starting or restarting the bot with the Start Action. +func (b *Behaviour) WithInit(a Action) *Behaviour { + b.Init = newAction(a) return b } -func (b *Behaviour) OnStartFunc( +func (b *Behaviour) WithInitFunc( fn ActionFunc, ) *Behaviour { - return b.WithStart(fn) + return b.WithInit(fn) } // The function sets screens. diff --git a/src/tx/bot.go b/src/tx/bot.go index e58549e..c92863d 100644 --- a/src/tx/bot.go +++ b/src/tx/bot.go @@ -1,8 +1,6 @@ package tx import ( - //"fmt" - "errors" apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" @@ -119,35 +117,36 @@ func (bot *Bot) handlePrivate(updates chan *Update) { chans := make(map[SessionId]chan *Update) var sid SessionId for u := range updates { - if u.Message != nil { - // Create new session if the one does not exist - // for this user. - sid = SessionId(u.Message.Chat.ID) - if _, ok := bot.sessions[sid]; !ok { - bot.sessions.Add(sid) - } + sid = SessionId(u.FromChat().ID) + var sessionOk, chnOk bool + // Create new session if the one does not exist + // for this user. + if _, sessionOk = bot.sessions[sid]; !sessionOk { + bot.sessions.Add(sid) + } - // The "start" command resets the bot - // by executing the Start Action. - if u.Message.IsCommand() { - cmdName := CommandName(u.Message.Command()) - if cmdName == "start" { - // Getting current session and context. - session := bot.sessions[sid] - ctx := &Context{ - B: bot, - Session: session, - updates: make(chan *Update), - } + _, chnOk = chans[sid] + // Making the bot ignore anything except "start" + // before the session started + if u.Message.IsCommand() && + (!sessionOk) { + cmdName := CommandName(u.Message.Command()) + if cmdName == "start" { + session := bot.sessions[sid] + ctx := &Context{ + B: bot, + Session: session, + updates: make(chan *Update), + } + // Starting the new goroutine if + // there is no one. + if !chnOk { chn := make(chan *Update) chans[sid] = chn - // Starting the goroutine for the user. go ctx.handleUpdateChan(chn) } } - } else if u.CallbackQuery != nil { - sid = SessionId(u.CallbackQuery.Message.Chat.ID) } chn, ok := chans[sid] // The bot MUST get the "start" command. diff --git a/src/tx/button.go b/src/tx/button.go index 89e1f3d..bbc78e4 100644 --- a/src/tx/button.go +++ b/src/tx/button.go @@ -10,7 +10,7 @@ type Button struct { Data string Url string SendLocation bool - Action Action + Action *action } type ButtonMap map[string]*Button @@ -34,7 +34,7 @@ func (btn *Button) WithUrl(url string) *Button { // Set the action when pressing the button. // By default is nil and does nothing. func (btn *Button) WithAction(a Action) *Button { - btn.Action = a + btn.Action = newAction(a) return btn } diff --git a/src/tx/command.go b/src/tx/command.go index 281280a..df2c93d 100644 --- a/src/tx/command.go +++ b/src/tx/command.go @@ -12,7 +12,7 @@ type CommandName string type Command struct { Name CommandName Description string - Action Action + Action *action } type CommandMap map[CommandName]*Command @@ -23,7 +23,7 @@ func NewCommand(name CommandName) *Command { } func (c *Command) WithAction(a Action) *Command { - c.Action = a + c.Action = newAction(a) return c } diff --git a/src/tx/context.go b/src/tx/context.go index 4c23abc..d68769b 100644 --- a/src/tx/context.go +++ b/src/tx/context.go @@ -23,7 +23,10 @@ func (c *Context) handleUpdateChan(updates chan *Update) { var act Action bot := c.B beh := bot.behaviour - c.run(beh.Start, nil) + + if beh.Init != nil { + c.run(beh.Init, nil) + } for u := range updates { screen := c.curScreen // The part is added to implement custom update handling. diff --git a/src/tx/encoding.go b/src/tx/encoding.go new file mode 100644 index 0000000..8896af8 --- /dev/null +++ b/src/tx/encoding.go @@ -0,0 +1,37 @@ +package tx + +import ( + "reflect" +) + +var ( + actionMapByReflect = make(map[reflect.Type]string) + actionMapByTypeName = make(map[string]reflect.Type) + Init func() +) + +func initEncoding() { + actions := map[string]Action{ + "action-func": ActionFunc(nil), + "screen-change": ScreenChange(""), + } + for k, action := range actions { + DefineAction(k, action) + } +} + +// Define interface to make it marshalable to JSON etc. +// Like in GOB. Must be done both on client and server +// if one is provided. +func DefineAction(typeName string, a Action) error { + t := reflect.TypeOf(a) + + actionMapByReflect[t] = typeName + actionMapByTypeName[typeName] = t + + return nil +} + +func DefineGroupAction(typ string, a GroupAction) error { + return nil +} diff --git a/src/tx/errors.go b/src/tx/errors.go index 2d5fe43..de7f982 100644 --- a/src/tx/errors.go +++ b/src/tx/errors.go @@ -15,6 +15,7 @@ var ( KeyboardNotExistErr = errors.New("keyboard does not exist") NotAvailableErr = errors.New("the context is not available") EmptyKeyboardTextErr = errors.New("got empty text for a keyboard") + ActionNotDefinedErr = errors.New("action was not defined") ) func (wut WrongUpdateType) Error() string { diff --git a/src/tx/json.go b/src/tx/json.go new file mode 100644 index 0000000..75a1ff8 --- /dev/null +++ b/src/tx/json.go @@ -0,0 +1,34 @@ +package tx + +import ( + "encoding/json" + "reflect" +) + +func (a *action) UnmarshalJSON(data []byte) error { + var err error + m := make(map[string]any) + err = json.Unmarshal(data, &m) + if err != nil { + return err + } + + bts, err := json.Marshal(m["Action"]) + if err != nil { + return err + } + + a.Type = m["Type"].(string) + typ := actionMapByTypeName[a.Type].(reflect.Type) + if typ.Kind() == reflect.Pointer { + typ = typ.Elem() + } + vr := reflect.New(typ).Interface().(Action) + err = json.Unmarshal(bts, vr) + if err != nil { + return err + } + + a.Action = vr + return nil +} diff --git a/src/tx/main.go b/src/tx/main.go index 8c5c56b..fe470ea 100644 --- a/src/tx/main.go +++ b/src/tx/main.go @@ -1,4 +1,5 @@ package tx -// The package implements behaviourial -// definition for the Telegram bots through the API. +func init() { + initEncoding() +} diff --git a/src/tx/screen.go b/src/tx/screen.go index 94b8097..9fd96f5 100644 --- a/src/tx/screen.go +++ b/src/tx/screen.go @@ -19,7 +19,7 @@ type Screen struct { // Keyboard to be displayed on the screen. Keyboard *Keyboard // Action called on the reaching the screen. - Action Action + Action *action } // Map structure for the screens. @@ -53,7 +53,7 @@ func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen { } func (s *Screen) WithAction(a Action) *Screen { - s.Action = a + s.Action = newAction(a) return s }