Added the Node notation to define paths to screens..

This commit is contained in:
Andrey Parhomenko 2023-09-21 12:03:54 +03:00
parent c96c2a7559
commit 061add76a8
7 changed files with 199 additions and 158 deletions

View file

@ -51,10 +51,7 @@ func ExtractSessionData(c *tg.Context) *SessionData {
} }
var ( var (
startScreenButton = tg.NewButton("Back").ActionFunc(func(c *tg.Context){ startScreenButton = tg.NewButton("Home").Go("/")
c.GoUp()
})
incDecKeyboard = tg.NewKeyboard().Row( incDecKeyboard = tg.NewKeyboard().Row(
tg.NewButton("+").ActionFunc(func(c *tg.Context) { tg.NewButton("+").ActionFunc(func(c *tg.Context) {
d := ExtractSessionData(c) d := ExtractSessionData(c)
@ -71,14 +68,14 @@ var (
) )
navKeyboard = tg.NewKeyboard().Row( navKeyboard = tg.NewKeyboard().Row(
tg.NewButton("Inc/Dec").ScreenChange("/start/inc-dec"), tg.NewButton("Inc/Dec").Go("/inc-dec"),
).Row( ).Row(
tg.NewButton("Upper case").ActionFunc(func(c *tg.Context){ tg.NewButton("Upper case").ActionFunc(func(c *tg.Context){
c.Go("/start/upper-case", "this shit", "works") c.Go("/upper-case", "this shit", "works")
}), }),
tg.NewButton("Lower case").ScreenChange("/start/lower-case"), tg.NewButton("Lower case").Go("/case"),
).Row( ).Row(
tg.NewButton("Send location").ScreenChange("/start/send-location"), tg.NewButton("Send location").Go("/send-location"),
).Reply().WithOneTime(true) ).Reply().WithOneTime(true)
sendLocationKeyboard = tg.NewKeyboard().Row( sendLocationKeyboard = tg.NewKeyboard().Row(
@ -106,124 +103,106 @@ var (
).Reply() ).Reply()
) )
var theNode = tg.NewNode(
"/", tg.WidgetFunc(func(c *tg.Context){
c.Go("/start")
}),
tg.NewNode(
"start", tg.WidgetFunc(func(c *tg.Context){}),
tg.NewNode(
"profile", tg.WidgetFunc(func(c *tg.Context){}),
),
tg.NewNode(
"upper-case", tg.WidgetFunc(func(c *tg.Context){}),
),
),
)
var beh = tg.NewBehaviour(). var beh = tg.NewBehaviour().
WithInitFunc(func(c *tg.Context) { WithInitFunc(func(c *tg.Context) {
// The session initialization. // The session initialization.
c.Session.Data = &SessionData{} c.Session.Data = &SessionData{}
}).WithScreens( }).WithRootNode(tg.NewRootNode(
tg.NewScreen("/start", tg.NewPage( // The "/" widget.
"", tg.NewPage().
).WithInline( WithInline(
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
tg.NewButton("GoT Github page"). tg.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"), WithUrl("https://github.com/mojosa-software/got"),
).Inline().Widget("The bot started!"), ).Inline().Widget("The bot started!"),
).WithReply( ).WithReply(
navKeyboard.Widget("Choose what you are interested in"), navKeyboard.Widget("Choose what you are interested in"),
),
), ),
tg.NewScreen("/start/inc-dec", tg.NewPage(
"The screen shows how "+ tg.NewNode(
"user separated data works "+ "inc-dec", tg.NewPage().WithReply(
"by saving the counter for each of users "+
"separately. ",
).WithReply(
incDecKeyboard.Reply().Widget("Press the buttons to increment and decrement"), incDecKeyboard.Reply().Widget("Press the buttons to increment and decrement"),
).ActionFunc(func(c *tg.Context) { ).ActionFunc(func(c *tg.Context) {
// The function will be calleb before serving page. // The function will be calleb before serving page.
d := ExtractSessionData(c) d := ExtractSessionData(c)
c.Sendf("Current counter value = %d", d.Counter) c.Sendf("Current counter value = %d", d.Counter)
}), }),
), ),
tg.NewScreen("/start/upper-case", tg.NewPage( tg.NewNode(
"upper-case", tg.NewPage().WithText(
"Type text and the bot will send you the upper case version to you", "Type text and the bot will send you the upper case version to you",
).WithReply( ).WithReply(
navToStartKeyboard.Widget(""), navToStartKeyboard.Widget(""),
).WithSub( ).WithSub(
NewMutateMessageWidget(strings.ToUpper), NewMutateMessageWidget(strings.ToUpper),
), ),
), ),
tg.NewScreen("/start/lower-case", tg.NewPage( tg.NewNode(
"lower-case", tg.NewPage().WithText(
"Type text and the bot will send you the lower case version", "Type text and the bot will send you the lower case version",
).WithReply( ).WithReply(
navToStartKeyboard.Widget(""), navToStartKeyboard.Widget(""),
).WithSub( ).WithSub(
NewMutateMessageWidget(strings.ToLower), NewMutateMessageWidget(strings.ToLower),
), ),
), ),
tg.NewScreen("/start/send-location", tg.NewPage( tg.NewNode(
"", "send-location", tg.NewPage().WithReply(
).WithReply( sendLocationKeyboard.Widget("Press the button to send your location!"),
sendLocationKeyboard.Widget("Press the button to send your location!"), ).WithInline(
).WithInline( tg.NewKeyboard().Row(
tg.NewKeyboard().Row( tg.NewButton(
tg.NewButton( "Check",
"Check", ).WithData(
).WithData( "check",
"check", ).ActionFunc(func(c *tg.Context) {
).ActionFunc(func(c *tg.Context) { d := ExtractSessionData(c)
d := ExtractSessionData(c) c.Sendf("Counter = %d", d.Counter)
c.Sendf("Counter = %d", d.Counter) }),
}), ).Inline().Widget("Press the button to display your counter"),
).Inline().Widget("Press the button to display your counter"),
),
), ),
).WithCommands( ),
tg.NewCommand("start"). )).WithCommands(
Desc("start or restart the bot or move to the start screen"). tg.NewCommand("start").
ActionFunc(func(c *tg.Context){ Desc("start or restart the bot or move to the start screen").
c.Sendf("Your username is %q", c.Message.From.UserName) ActionFunc(func(c *tg.Context){
c.Go("/start") c.Sendf("Your username is %q", c.Message.From.UserName)
}), c.Go("/start")
tg.NewCommand("hello"). }),
Desc("sends the 'Hello, World!' message back"). tg.NewCommand("hello").
ActionFunc(func(c *tg.Context) { Desc("sends the 'Hello, World!' message back").
c.Sendf("Hello, World!") ActionFunc(func(c *tg.Context) {
}), c.Sendf("Hello, World!")
tg.NewCommand("read"). }),
Desc("reads a string and sends it back"). tg.NewCommand("read").
WidgetFunc(func(c *tg.Context) { Desc("reads a string and sends it back").
c.Sendf("Type text and I will send it back to you") WidgetFunc(func(c *tg.Context) {
for u := range c.Input() { c.Sendf("Type text and I will send it back to you")
if u.Message == nil { for u := range c.Input() {
continue if u.Message == nil {
} continue
c.Sendf("You typed %q", u.Message.Text)
break
} }
c.Sendf("Done") c.Sendf("You typed %q", u.Message.Text)
}), break
tg.NewCommand("image"). }
Desc("sends a sample image"). c.Sendf("Done")
ActionFunc(func(c *tg.Context) { }),
img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!") tg.NewCommand("image").
c.Send(img) Desc("sends a sample image").
}), ActionFunc(func(c *tg.Context) {
tg.NewCommand("botname"). img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!")
Desc("get the bot name"). c.Send(img)
ActionFunc(func(c *tg.Context) { }),
bd := c.Bot.Data.(*BotData) tg.NewCommand("botname").
c.Sendf("My name is %q", bd.Name) Desc("get the bot name").
}), ActionFunc(func(c *tg.Context) {
) bd := c.Bot.Data.(*BotData)
c.Sendf("My name is %q", bd.Name)
}),
)
var gBeh = tg.NewGroupBehaviour(). var gBeh = tg.NewGroupBehaviour().
InitFunc(func(c *tg.GC) { InitFunc(func(c *tg.GC) {
@ -239,8 +218,7 @@ var gBeh = tg.NewGroupBehaviour().
) )
func main() { func main() {
log.Println(theNode.ScreenMap()) log.Println(beh.Screens)
return
token := os.Getenv("BOT_TOKEN") token := os.Getenv("BOT_TOKEN")
bot, err := tg.NewBot(token) bot, err := tg.NewBot(token)

View file

@ -37,8 +37,13 @@ func (b *Behaviour) WithInitFunc(
return b.WithInit(fn) return b.WithInit(fn)
} }
func (b *Behaviour) WithRootNode(node *RootNode) *Behaviour {
b.Screens = node.ScreenMap()
return b
}
// The function sets screens. // The function sets screens.
func (b *Behaviour) WithScreens( /*func (b *Behaviour) WithScreens(
screens ...*Screen, screens ...*Screen,
) *Behaviour { ) *Behaviour {
for _, screen := range screens { for _, screen := range screens {
@ -52,7 +57,7 @@ func (b *Behaviour) WithScreens(
b.Screens[screen.Id] = screen b.Screens[screen.Id] = screen
} }
return b return b
} }*/
// The function sets as the standard root widget CommandWidget // The function sets as the standard root widget CommandWidget
// and its commands.. // and its commands..
@ -69,18 +74,19 @@ func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour {
} }
// Check whether the screen exists in the behaviour. // Check whether the screen exists in the behaviour.
func (beh *Behaviour) ScreenExist(id ScreenId) bool { func (beh *Behaviour) PathExist(pth Path) bool {
_, ok := beh.Screens[id] _, ok := beh.Screens[pth]
return ok return ok
} }
// Returns the screen by it's ID. // Returns the screen by it's ID.
func (beh *Behaviour) GetScreen(id ScreenId) *Screen { func (beh *Behaviour) GetScreen(pth Path) *Screen {
if !beh.ScreenExist(id) { pth = pth.Clean()
if !beh.PathExist(pth) {
panic(ScreenNotExistErr) panic(ScreenNotExistErr)
} }
screen := beh.Screens[id] screen := beh.Screens[pth]
return screen return screen
} }

View file

@ -63,7 +63,7 @@ func (btn *Button) ActionFunc(fn ActionFunc) *Button {
return btn.WithAction(fn) return btn.WithAction(fn)
} }
func (btn *Button) ScreenChange(sc ScreenChange) *Button { func (btn *Button) Go(sc ScreenChange) *Button {
return btn.WithAction(sc) return btn.WithAction(sc)
} }

View file

@ -165,7 +165,7 @@ func (widget *CommandWidget) Serve(c *Context) {
var cmdUpdates *UpdateChan var cmdUpdates *UpdateChan
for u := range c.Input() { for u := range c.Input() {
if c.CurScreen() == "" && u.Message != nil { if c.Path() == "" && u.Message != nil {
// Skipping and executing the preinit action // Skipping and executing the preinit action
// while we have the empty screen. // while we have the empty screen.
// E. g. the session did not start. // E. g. the session did not start.

View file

@ -15,12 +15,16 @@ type Page struct {
} }
// Return new page with the specified text. // Return new page with the specified text.
func NewPage(text string) *Page { func NewPage() *Page {
ret := &Page{} ret := &Page{}
ret.Text = text
return ret return ret
} }
func (p *Page) WithText(text string) *Page {
p.Text = text
return p
}
// Set the inline keyboard. // Set the inline keyboard.
func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page { func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page {
p.Inline = inline p.Inline = inline

View file

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"path" //"path"
) )
type ContextType string type ContextType string
@ -25,7 +25,7 @@ type context struct {
Bot *Bot Bot *Bot
skippedUpdates *UpdateChan skippedUpdates *UpdateChan
// Current screen ID. // Current screen ID.
screenId, prevScreenId ScreenId path, prevPath Path
} }
// Goroutie function to handle each user. // Goroutie function to handle each user.
@ -42,17 +42,17 @@ func (c *context) run(a Action, u *Update) {
a.Act(&Context{context: c, Update: u}) a.Act(&Context{context: c, Update: u})
} }
func (c *Context) CurScreen() ScreenId { func (c *Context) Path() Path {
return c.screenId return c.path
} }
func (c *Context) PrevScreen() ScreenId { func (c *Context) PrevPath() Path {
return c.prevScreenId return c.prevPath
} }
func (c *Context) Run(a Action, u *Update) { func (c *Context) Run(a Action, u *Update) {
if a != nil { if a != nil {
a.Act(&Context{context: c.context, Update: u}) a.Act(c.Copy().WithUpdate(u))
} }
} }
@ -144,13 +144,13 @@ func (af ActionFunc) Act(c *Context) {
} }
// The type implements changing screen to the underlying ScreenId // The type implements changing screen to the underlying ScreenId
type ScreenChange ScreenId type ScreenChange Path
func (sc ScreenChange) Act(c *Context) { func (sc ScreenChange) Act(c *Context) {
if !c.Bot.behaviour.ScreenExist(ScreenId(sc)) { if !c.Bot.behaviour.PathExist(Path(sc)) {
panic(ScreenNotExistErr) panic(ScreenNotExistErr)
} }
err := c.Go(ScreenId(sc)) err := c.Go(Path(sc))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -159,18 +159,22 @@ func (sc ScreenChange) Act(c *Context) {
type C = Context type C = Context
// Changes screen of user to the Id one. // Changes screen of user to the Id one.
func (c *Context) Go(screenId ScreenId, args ...any) error { func (c *Context) Go(pth Path, args ...any) error {
if !c.Bot.behaviour.ScreenExist(screenId) { if !c.PathExist(pth) {
return ScreenNotExistErr return ScreenNotExistErr
} }
// Getting the screen and changing to // Getting the screen and changing to
// then executing its widget. // then executing its widget.
screen := c.Bot.behaviour.Screens[screenId] if !pth.IsAbs() {
c.prevScreenId = c.screenId pth = (c.Path() + "/" + pth).Clean()
c.screenId = screenId }
c.prevPath = c.path
c.path = pth
// Stopping the current widget. // Stopping the current widget.
screen := c.Bot.behaviour.Screens[pth]
c.skippedUpdates.Close() c.skippedUpdates.Close()
if screen.Widget != nil { if screen.Widget != nil {
c.skippedUpdates = c.RunWidget(screen.Widget, args...) c.skippedUpdates = c.RunWidget(screen.Widget, args...)
@ -181,6 +185,10 @@ func (c *Context) Go(screenId ScreenId, args ...any) error {
return nil return nil
} }
func (c *Context) PathExist(pth Path) bool {
return c.Bot.behaviour.PathExist(pth)
}
// Run widget in background returning the new input channel for it. // Run widget in background returning the new input channel for it.
func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan { func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan {
if widget == nil { if widget == nil {
@ -209,12 +217,18 @@ func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan {
return updates return updates
} }
// Go to the root screen.
func (c *Context) GoRoot() {
c.Go("/")
}
// Go one level upper in the screen hierarchy.
func (c *Context) GoUp() { func (c *Context) GoUp() {
c.Go(ScreenId(path.Dir(string(c.CurScreen())))) c.Go(c.Path().Dir())
} }
// Change screen to the previous. // Change screen to the previous.
// To get to the parent screen use GoUp. // To get to the parent screen use GoUp.
func (c *Context) GoPrev() { func (c *Context) GoPrev() {
c.Go(c.PrevScreen()) c.Go(c.PrevPath())
} }

View file

@ -1,60 +1,100 @@
package tg package tg
// Unique identifier for the screen. import (
type ScreenId string "path"
)
// Unique identifier for the screen
// and relative paths to the screen.
type Path string
// Returns true if the path is empty.
func (p Path) IsEmpty() bool {
return p == ""
}
// Returns true if the path is absolute.
func (p Path) IsAbs() bool {
if len(p) == 0 {
return false
}
return p[0] == '/'
}
func (p Path) Dir() Path {
return Path(path.Dir(string(p)))
}
// Clean the path deleting exceed ., .. and / .
func (p Path) Clean() Path {
return Path(path.Clean(string(p)))
}
// Screen statement of the bot. // Screen statement of the bot.
// Mostly what buttons to show. // Mostly what buttons to show.
type Screen struct { type Screen struct {
// Unique identifer to change to the screen
// via Context.ChangeScreen method.
Id ScreenId
// The widget to run when reaching the screen. // The widget to run when reaching the screen.
Widget Widget Widget Widget
} }
// The first node with the "/" path.
type RootNode struct {
Screen *Screen
Subs []*Node
}
// The node is a simple way to represent // The node is a simple way to represent
// tree-like structured applications. // tree-like structured applications.
type Node struct { type Node struct {
Path Path
Screen *Screen Screen *Screen
Subs []*Node Subs []*Node
} }
func NewNode(id ScreenId, widget Widget, subs ...*Node) *Node { // Return new root node with the specified widget in the screen.
ret := &Node{} func NewRootNode(widget Widget, subs ...*Node) *RootNode {
ret.Screen = NewScreen(id, widget) ret := &RootNode{}
ret.Screen = NewScreen(widget)
ret.Subs = subs ret.Subs = subs
return ret return ret
} }
func (n *Node) ScreenMap() ScreenMap { func NewNode(relPath Path, widget Widget, subs ...*Node) *Node {
ret := &Node{}
ret.Path = relPath
ret.Screen = NewScreen(widget)
ret.Subs = subs
return ret
}
func (n *RootNode) ScreenMap() ScreenMap {
m := make(ScreenMap) m := make(ScreenMap)
id := n.Screen.Id var root Path = "/"
m[id] = n.Screen m[root] = n.Screen
n.Screen.Id = id
var root ScreenId
if id == "/" {
root = ""
} else {
root = id
}
for _, sub := range n.Subs { for _, sub := range n.Subs {
buf := sub.screenMap(root + "/") buf := sub.ScreenMap(root)
for k, v := range buf { for k, v := range buf {
_, ok := m[k]
if ok {
panic("duplicate paths in node definition")
}
m[k] = v m[k] = v
} }
} }
return m return m
} }
func (n *Node) screenMap(root ScreenId) ScreenMap { func (n *Node) ScreenMap(root Path) ScreenMap {
m := make(ScreenMap) m := make(ScreenMap)
id := root+ n.Screen.Id pth := (root + n.Path).Clean()
m[id] = n.Screen m[pth] = n.Screen
n.Screen.Id = id
for _, sub := range n.Subs { for _, sub := range n.Subs {
buf := sub.screenMap(id + "/") buf := sub.ScreenMap((pth + "/").Clean())
for k, v := range buf { for k, v := range buf {
_, ok := m[k]
if ok {
panic("duplicate paths in node definition")
}
m[k] = v m[k] = v
} }
} }
@ -62,12 +102,11 @@ func (n *Node) screenMap(root ScreenId) ScreenMap {
} }
// Map structure for the screens. // Map structure for the screens.
type ScreenMap map[ScreenId]*Screen type ScreenMap map[Path] *Screen
// Returns the new screen with specified name and widget. // Returns the new screen with specified name and widget.
func NewScreen(id ScreenId, widget Widget) *Screen { func NewScreen(widget Widget) *Screen {
return &Screen{ return &Screen{
Id: id,
Widget: widget, Widget: widget,
} }
} }