Finally implemented JSON-ing of behaviours.

This commit is contained in:
Andrey Parhomenko 2023-08-16 17:25:56 +03:00
parent 772adb7b8b
commit 94d8c38dd5
14 changed files with 257 additions and 143 deletions

View file

@ -18,9 +18,18 @@ type UserData struct {
Counter int 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 var err error
fmt.Println("In Act") fmt.Println("In Act")
e := env.NewEnv() e := env.NewEnv()
@ -32,52 +41,46 @@ func (c Code) Act(a *tx.A) {
panic(err) panic(err)
} }
_, err = vm.Execute(e, nil, string(c)) _, err = vm.Execute(e, nil, c.Code)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
var startScreenButton = tx.NewButton("🏠 To the start screen"). func main() {
WithAction(Code(` tx.DefineAction("goscript", &Code{})
var startScreenButton = tx.NewButton("🏠 To the start screen").
WithAction(NewCode(`
a.ChangeScreen("start") a.ChangeScreen("start")
`)) `))
var beh = tx.NewBehaviour(). var (
incDecKeyboard = tx.NewKeyboard("").Row(
// The function will be called every time tx.NewButton("+").WithAction(NewCode(`
// 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(`
d = a.V d = a.V
d.Counter++ d.Counter++
a.Sendf("%d", d.Counter) a.Sendf("%d", d.Counter)
`)), `)),
tx.NewButton("-").WithAction(Code(` tx.NewButton("-").WithAction(NewCode(`
d = a.V d = a.V
d.Counter-- d.Counter--
a.Sendf("%d", d.Counter) a.Sendf("%d", d.Counter)
`)), `)),
).Row( ).Row(
startScreenButton, startScreenButton,
), )
// The navigational keyboard. // The navigational keyboard.
tx.NewKeyboard("nav").Row( navKeyboard = tx.NewKeyboard("").Row(
tx.NewButton("Inc/Dec").WithAction(Code(`a.ChangeScreen("inc/dec")`)), tx.NewButton("Inc/Dec").WithAction(NewCode(`a.ChangeScreen("inc/dec")`)),
).Row( ).Row(
tx.NewButton("Upper case").WithAction(Code(`a.ChangeScreen("upper-case")`)), tx.NewButton("Upper case").WithAction(NewCode(`a.ChangeScreen("upper-case")`)),
tx.NewButton("Lower case").WithAction(Code(`a.ChangeScreen("lower-case")`)), tx.NewButton("Lower case").WithAction(NewCode(`a.ChangeScreen("lower-case")`)),
).Row( ).Row(
tx.NewButton("Send location"). tx.NewButton("Send location").
WithSendLocation(true). WithSendLocation(true).
WithAction(Code(` WithAction(NewCode(`
err = nil err = nil
if a.U.Message.Location != nil { if a.U.Message.Location != nil {
l = a.U.Message.Location l = a.U.Message.Location
@ -89,18 +92,25 @@ var beh = tx.NewBehaviour().
a.Send(err) a.Send(err)
} }
`)), `)),
), )
tx.NewKeyboard("istart").Row( inlineKeyboard = tx.NewKeyboard("").Row(
tx.NewButton("My Telegram"). tx.NewButton("My Telegram").
WithUrl("https://t.me/surdeus"), WithUrl("https://t.me/surdeus"),
), )
// The keyboard to return to the start screen. // The keyboard to return to the start screen.
tx.NewKeyboard("nav-start").Row( navToStartKeyboard = tx.NewKeyboard("nav-start").Row(
startScreenButton, startScreenButton,
), )
).WithScreens( )
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"). tx.NewScreen("start").
WithText( WithText(
"The bot started!"+ "The bot started!"+
@ -108,8 +118,8 @@ var beh = tx.NewBehaviour().
" understand of how the API works, so just"+ " understand of how the API works, so just"+
" horse around a bit to guess everything out"+ " horse around a bit to guess everything out"+
" by yourself!", " by yourself!",
).Keyboard("nav"). ).WithKeyboard(navKeyboard).
IKeyboard("istart"), WithIKeyboard(inlineKeyboard),
tx.NewScreen("inc/dec"). tx.NewScreen("inc/dec").
WithText( WithText(
@ -118,17 +128,17 @@ var beh = tx.NewBehaviour().
"by saving the counter for each of users "+ "by saving the counter for each of users "+
"separately. ", "separately. ",
). ).
Keyboard("inc/dec"). WithKeyboard(incDecKeyboard).
// The function will be called when reaching the screen. // The function will be called when reaching the screen.
WithAction(Code(` WithAction(NewCode(`
d = a.V d = a.V
a.Sendf("Current counter value = %d", d.Counter) a.Sendf("Current counter value = %d", d.Counter)
`)), `)),
tx.NewScreen("upper-case"). tx.NewScreen("upper-case").
WithText("Type text and the bot will send you the upper case version to you"). WithText("Type text and the bot will send you the upper case version to you").
Keyboard("nav-start"). WithKeyboard(navToStartKeyboard).
WithAction(Code(` WithAction(NewCode(`
strings = import("strings") strings = import("strings")
for { for {
msg, err = a.ReadTextMessage() msg, err = a.ReadTextMessage()
@ -147,8 +157,8 @@ var beh = tx.NewBehaviour().
tx.NewScreen("lower-case"). tx.NewScreen("lower-case").
WithText("Type text and the bot will send you the lower case version"). WithText("Type text and the bot will send you the lower case version").
Keyboard("nav-start"). WithKeyboard(navToStartKeyboard).
WithAction(Code(` WithAction(NewCode(`
strings = import("strings") strings = import("strings")
for { for {
msg, err = a.ReadTextMessage() msg, err = a.ReadTextMessage()
@ -164,15 +174,20 @@ var beh = tx.NewBehaviour().
} }
} }
`)), `)),
).WithCommands( ).WithCommands(
tx.NewCommand("start").
Desc("start or restart the bot").
WithAction(NewCode(`
a.ChangeScreen("start")
`)),
tx.NewCommand("hello"). tx.NewCommand("hello").
Desc("sends the 'Hello, World!' message back"). Desc("sends the 'Hello, World!' message back").
WithAction(Code(` WithAction(NewCode(`
a.Send("Hello, World!") a.Send("Hello, World!")
`)), `)),
tx.NewCommand("read"). tx.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
WithAction(Code(` WithAction(NewCode(`
a.Send("Type some text:") a.Send("Type some text:")
msg, err = a.ReadTextMessage() msg, err = a.ReadTextMessage()
if err != nil { if err != nil {
@ -180,26 +195,26 @@ var beh = tx.NewBehaviour().
} }
a.Sendf("You typed %q", msg) a.Sendf("You typed %q", msg)
`)), `)),
) )
func main() {
bts, err := json.MarshalIndent(beh, "", "\t") bts, err := json.MarshalIndent(beh, "", "\t")
if err != nil { if err != nil {
panic(err) panic(err)
} }
fmt.Printf("%s", bts) fmt.Printf("%s", bts)
/*jBeh := &tx.Behaviour{} jBeh := &tx.Behaviour{}
err = json.Unmarshal(bts, jBeh) err = json.Unmarshal(bts, jBeh)
if err != nil { if err != nil {
panic(err) panic(err)
}*/ }
bot, err := tx.NewBot(os.Getenv("BOT_TOKEN"), beh, nil) bot, err := tx.NewBot(os.Getenv("BOT_TOKEN"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
bot = bot.WithBehaviour(jBeh)
err = bot.Run() err = bot.Run()
if err != nil { if err != nil {
panic(err) panic(err)

5
cmd/json/mkfile Normal file
View file

@ -0,0 +1,5 @@
all:
go build
run:V:
./jsoned

View file

@ -77,11 +77,11 @@ var (
) )
var beh = tx.NewBehaviour(). var beh = tx.NewBehaviour().
OnStartFunc(func(c *tx.A) { WithInitFunc(func(c *tx.A) {
// The function will be called every time // The session initialization.
// the bot is started.
c.V = &UserData{} c.V = &UserData{}
c.ChangeScreen("start") c.ChangeScreen("start")
}).WithScreens( }).WithScreens(
tx.NewScreen("start"). tx.NewScreen("start").
WithText( WithText(

View file

@ -1,17 +1,43 @@
package tx 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 { type Action interface {
Act(*Arg) Act(*Arg)
} }
// Customized actions for the
type GroupAction interface { type GroupAction interface {
Act(*GroupArg) Act(*GroupArg)
} }
// Customized actions for the bot.
type ActionFunc func(*Arg) type ActionFunc func(*Arg)
func (af ActionFunc) Act(a *Arg) { func (af ActionFunc) Act(a *Arg) {
@ -99,12 +125,3 @@ type ChannelAction struct {
type JsonTyper interface { type JsonTyper interface {
JsonType() string JsonType() string
} }
type JsonAction struct {
Type string
Action Action
}
func (ja JsonAction) UnmarshalJSON(bts []byte, ptr any) error {
return nil
}

View file

@ -5,7 +5,7 @@ package tx
// The type describes behaviour for the bot in personal chats. // The type describes behaviour for the bot in personal chats.
type Behaviour struct { type Behaviour struct {
Start Action Init *action
Screens ScreenMap Screens ScreenMap
Keyboards KeyboardMap Keyboards KeyboardMap
Commands CommandMap Commands CommandMap
@ -20,15 +20,17 @@ func NewBehaviour() *Behaviour {
} }
} }
func (b *Behaviour) WithStart(a Action) *Behaviour { // The Action will be called on session creation,
b.Start = a // not when starting or restarting the bot with the Start Action.
func (b *Behaviour) WithInit(a Action) *Behaviour {
b.Init = newAction(a)
return b return b
} }
func (b *Behaviour) OnStartFunc( func (b *Behaviour) WithInitFunc(
fn ActionFunc, fn ActionFunc,
) *Behaviour { ) *Behaviour {
return b.WithStart(fn) return b.WithInit(fn)
} }
// The function sets screens. // The function sets screens.

View file

@ -1,8 +1,6 @@
package tx package tx
import ( import (
//"fmt"
"errors" "errors"
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" apix "github.com/go-telegram-bot-api/telegram-bot-api/v5"
@ -119,20 +117,21 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
chans := make(map[SessionId]chan *Update) chans := make(map[SessionId]chan *Update)
var sid SessionId var sid SessionId
for u := range updates { for u := range updates {
if u.Message != nil { sid = SessionId(u.FromChat().ID)
var sessionOk, chnOk bool
// Create new session if the one does not exist // Create new session if the one does not exist
// for this user. // for this user.
sid = SessionId(u.Message.Chat.ID) if _, sessionOk = bot.sessions[sid]; !sessionOk {
if _, ok := bot.sessions[sid]; !ok {
bot.sessions.Add(sid) bot.sessions.Add(sid)
} }
// The "start" command resets the bot _, chnOk = chans[sid]
// by executing the Start Action. // Making the bot ignore anything except "start"
if u.Message.IsCommand() { // before the session started
if u.Message.IsCommand() &&
(!sessionOk) {
cmdName := CommandName(u.Message.Command()) cmdName := CommandName(u.Message.Command())
if cmdName == "start" { if cmdName == "start" {
// Getting current session and context.
session := bot.sessions[sid] session := bot.sessions[sid]
ctx := &Context{ ctx := &Context{
B: bot, B: bot,
@ -140,14 +139,14 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
updates: make(chan *Update), updates: make(chan *Update),
} }
// Starting the new goroutine if
// there is no one.
if !chnOk {
chn := make(chan *Update) chn := make(chan *Update)
chans[sid] = chn chans[sid] = chn
// Starting the goroutine for the user.
go ctx.handleUpdateChan(chn) go ctx.handleUpdateChan(chn)
} }
} }
} else if u.CallbackQuery != nil {
sid = SessionId(u.CallbackQuery.Message.Chat.ID)
} }
chn, ok := chans[sid] chn, ok := chans[sid]
// The bot MUST get the "start" command. // The bot MUST get the "start" command.

View file

@ -10,7 +10,7 @@ type Button struct {
Data string Data string
Url string Url string
SendLocation bool SendLocation bool
Action Action Action *action
} }
type ButtonMap map[string]*Button type ButtonMap map[string]*Button
@ -34,7 +34,7 @@ func (btn *Button) WithUrl(url string) *Button {
// Set the action when pressing the button. // Set the action when pressing the button.
// By default is nil and does nothing. // By default is nil and does nothing.
func (btn *Button) WithAction(a Action) *Button { func (btn *Button) WithAction(a Action) *Button {
btn.Action = a btn.Action = newAction(a)
return btn return btn
} }

View file

@ -12,7 +12,7 @@ type CommandName string
type Command struct { type Command struct {
Name CommandName Name CommandName
Description string Description string
Action Action Action *action
} }
type CommandMap map[CommandName]*Command type CommandMap map[CommandName]*Command
@ -23,7 +23,7 @@ func NewCommand(name CommandName) *Command {
} }
func (c *Command) WithAction(a Action) *Command { func (c *Command) WithAction(a Action) *Command {
c.Action = a c.Action = newAction(a)
return c return c
} }

View file

@ -23,7 +23,10 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
var act Action var act Action
bot := c.B bot := c.B
beh := bot.behaviour beh := bot.behaviour
c.run(beh.Start, nil)
if beh.Init != nil {
c.run(beh.Init, nil)
}
for u := range updates { for u := range updates {
screen := c.curScreen screen := c.curScreen
// The part is added to implement custom update handling. // The part is added to implement custom update handling.

37
src/tx/encoding.go Normal file
View file

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

View file

@ -15,6 +15,7 @@ var (
KeyboardNotExistErr = errors.New("keyboard does not exist") KeyboardNotExistErr = errors.New("keyboard does not exist")
NotAvailableErr = errors.New("the context is not available") NotAvailableErr = errors.New("the context is not available")
EmptyKeyboardTextErr = errors.New("got empty text for a keyboard") EmptyKeyboardTextErr = errors.New("got empty text for a keyboard")
ActionNotDefinedErr = errors.New("action was not defined")
) )
func (wut WrongUpdateType) Error() string { func (wut WrongUpdateType) Error() string {

34
src/tx/json.go Normal file
View file

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

View file

@ -1,4 +1,5 @@
package tx package tx
// The package implements behaviourial func init() {
// definition for the Telegram bots through the API. initEncoding()
}

View file

@ -19,7 +19,7 @@ type Screen struct {
// Keyboard to be displayed on the screen. // Keyboard to be displayed on the screen.
Keyboard *Keyboard Keyboard *Keyboard
// Action called on the reaching the screen. // Action called on the reaching the screen.
Action Action Action *action
} }
// Map structure for the screens. // Map structure for the screens.
@ -53,7 +53,7 @@ func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen {
} }
func (s *Screen) WithAction(a Action) *Screen { func (s *Screen) WithAction(a Action) *Screen {
s.Action = a s.Action = newAction(a)
return s return s
} }