From 061add76a88d49b32c7408b23c1a365888cb66dd Mon Sep 17 00:00:00 2001 From: surdeus Date: Thu, 21 Sep 2023 12:03:54 +0300 Subject: [PATCH] Added the Node notation to define paths to screens.. --- cmd/test/main.go | 184 +++++++++++++++++++++-------------------------- tg/beh.go | 20 ++++-- tg/button.go | 2 +- tg/command.go | 2 +- tg/page.go | 8 ++- tg/private.go | 48 ++++++++----- tg/screen.go | 93 +++++++++++++++++------- 7 files changed, 199 insertions(+), 158 deletions(-) diff --git a/cmd/test/main.go b/cmd/test/main.go index bc30ddb..f486739 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -51,10 +51,7 @@ func ExtractSessionData(c *tg.Context) *SessionData { } var ( - startScreenButton = tg.NewButton("Back").ActionFunc(func(c *tg.Context){ - c.GoUp() - }) - + startScreenButton = tg.NewButton("Home").Go("/") incDecKeyboard = tg.NewKeyboard().Row( tg.NewButton("+").ActionFunc(func(c *tg.Context) { d := ExtractSessionData(c) @@ -71,14 +68,14 @@ var ( ) navKeyboard = tg.NewKeyboard().Row( - tg.NewButton("Inc/Dec").ScreenChange("/start/inc-dec"), + tg.NewButton("Inc/Dec").Go("/inc-dec"), ).Row( 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( - tg.NewButton("Send location").ScreenChange("/start/send-location"), + tg.NewButton("Send location").Go("/send-location"), ).Reply().WithOneTime(true) sendLocationKeyboard = tg.NewKeyboard().Row( @@ -106,124 +103,106 @@ var ( ).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(). - WithInitFunc(func(c *tg.Context) { - // The session initialization. - c.Session.Data = &SessionData{} - }).WithScreens( - tg.NewScreen("/start", tg.NewPage( - "", - ).WithInline( - tg.NewKeyboard().Row( - tg.NewButton("GoT Github page"). - WithUrl("https://github.com/mojosa-software/got"), - ).Inline().Widget("The bot started!"), - ).WithReply( - navKeyboard.Widget("Choose what you are interested in"), - ), +WithInitFunc(func(c *tg.Context) { + // The session initialization. + c.Session.Data = &SessionData{} +}).WithRootNode(tg.NewRootNode( + // The "/" widget. + tg.NewPage(). + WithInline( + tg.NewKeyboard().Row( + tg.NewButton("GoT Github page"). + WithUrl("https://github.com/mojosa-software/got"), + ).Inline().Widget("The bot started!"), + ).WithReply( + navKeyboard.Widget("Choose what you are interested in"), ), - tg.NewScreen("/start/inc-dec", tg.NewPage( - "The screen shows how "+ - "user separated data works "+ - "by saving the counter for each of users "+ - "separately. ", - ).WithReply( + + tg.NewNode( + "inc-dec", tg.NewPage().WithReply( incDecKeyboard.Reply().Widget("Press the buttons to increment and decrement"), ).ActionFunc(func(c *tg.Context) { // The function will be calleb before serving page. d := ExtractSessionData(c) 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", ).WithReply( navToStartKeyboard.Widget(""), ).WithSub( 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", ).WithReply( navToStartKeyboard.Widget(""), ).WithSub( NewMutateMessageWidget(strings.ToLower), ), - ), + ), - tg.NewScreen("/start/send-location", tg.NewPage( - "", - ).WithReply( - sendLocationKeyboard.Widget("Press the button to send your location!"), - ).WithInline( - tg.NewKeyboard().Row( - tg.NewButton( - "Check", - ).WithData( - "check", - ).ActionFunc(func(c *tg.Context) { - d := ExtractSessionData(c) - c.Sendf("Counter = %d", d.Counter) - }), - ).Inline().Widget("Press the button to display your counter"), - ), + tg.NewNode( + "send-location", tg.NewPage().WithReply( + sendLocationKeyboard.Widget("Press the button to send your location!"), + ).WithInline( + tg.NewKeyboard().Row( + tg.NewButton( + "Check", + ).WithData( + "check", + ).ActionFunc(func(c *tg.Context) { + d := ExtractSessionData(c) + c.Sendf("Counter = %d", d.Counter) + }), + ).Inline().Widget("Press the button to display your counter"), ), - ).WithCommands( - tg.NewCommand("start"). - Desc("start or restart the bot or move to the start screen"). - ActionFunc(func(c *tg.Context){ - c.Sendf("Your username is %q", c.Message.From.UserName) - c.Go("/start") - }), - tg.NewCommand("hello"). - Desc("sends the 'Hello, World!' message back"). - ActionFunc(func(c *tg.Context) { - c.Sendf("Hello, World!") - }), - tg.NewCommand("read"). - Desc("reads a string and sends it back"). - WidgetFunc(func(c *tg.Context) { - c.Sendf("Type text and I will send it back to you") - for u := range c.Input() { - if u.Message == nil { - continue - } - c.Sendf("You typed %q", u.Message.Text) - break + ), +)).WithCommands( + tg.NewCommand("start"). + Desc("start or restart the bot or move to the start screen"). + ActionFunc(func(c *tg.Context){ + c.Sendf("Your username is %q", c.Message.From.UserName) + c.Go("/start") + }), + tg.NewCommand("hello"). + Desc("sends the 'Hello, World!' message back"). + ActionFunc(func(c *tg.Context) { + c.Sendf("Hello, World!") + }), + tg.NewCommand("read"). + Desc("reads a string and sends it back"). + WidgetFunc(func(c *tg.Context) { + c.Sendf("Type text and I will send it back to you") + for u := range c.Input() { + if u.Message == nil { + continue } - c.Sendf("Done") - }), - tg.NewCommand("image"). - Desc("sends a sample image"). - ActionFunc(func(c *tg.Context) { - img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!") - c.Send(img) - }), - tg.NewCommand("botname"). - Desc("get the bot name"). - ActionFunc(func(c *tg.Context) { - bd := c.Bot.Data.(*BotData) - c.Sendf("My name is %q", bd.Name) - }), - ) + c.Sendf("You typed %q", u.Message.Text) + break + } + c.Sendf("Done") + }), + tg.NewCommand("image"). + Desc("sends a sample image"). + ActionFunc(func(c *tg.Context) { + img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!") + c.Send(img) + }), + tg.NewCommand("botname"). + 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(). InitFunc(func(c *tg.GC) { @@ -239,8 +218,7 @@ var gBeh = tg.NewGroupBehaviour(). ) func main() { - log.Println(theNode.ScreenMap()) - return + log.Println(beh.Screens) token := os.Getenv("BOT_TOKEN") bot, err := tg.NewBot(token) diff --git a/tg/beh.go b/tg/beh.go index 633cf1d..027d5b2 100644 --- a/tg/beh.go +++ b/tg/beh.go @@ -37,8 +37,13 @@ func (b *Behaviour) WithInitFunc( return b.WithInit(fn) } +func (b *Behaviour) WithRootNode(node *RootNode) *Behaviour { + b.Screens = node.ScreenMap() + return b +} + // The function sets screens. -func (b *Behaviour) WithScreens( +/*func (b *Behaviour) WithScreens( screens ...*Screen, ) *Behaviour { for _, screen := range screens { @@ -52,7 +57,7 @@ func (b *Behaviour) WithScreens( b.Screens[screen.Id] = screen } return b -} +}*/ // The function sets as the standard root widget CommandWidget // and its commands.. @@ -69,18 +74,19 @@ func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour { } // Check whether the screen exists in the behaviour. -func (beh *Behaviour) ScreenExist(id ScreenId) bool { - _, ok := beh.Screens[id] +func (beh *Behaviour) PathExist(pth Path) bool { + _, ok := beh.Screens[pth] return ok } // Returns the screen by it's ID. -func (beh *Behaviour) GetScreen(id ScreenId) *Screen { - if !beh.ScreenExist(id) { +func (beh *Behaviour) GetScreen(pth Path) *Screen { + pth = pth.Clean() + if !beh.PathExist(pth) { panic(ScreenNotExistErr) } - screen := beh.Screens[id] + screen := beh.Screens[pth] return screen } diff --git a/tg/button.go b/tg/button.go index 77c1232..0863db8 100644 --- a/tg/button.go +++ b/tg/button.go @@ -63,7 +63,7 @@ func (btn *Button) ActionFunc(fn ActionFunc) *Button { return btn.WithAction(fn) } -func (btn *Button) ScreenChange(sc ScreenChange) *Button { +func (btn *Button) Go(sc ScreenChange) *Button { return btn.WithAction(sc) } diff --git a/tg/command.go b/tg/command.go index 396fc59..e4764db 100644 --- a/tg/command.go +++ b/tg/command.go @@ -165,7 +165,7 @@ func (widget *CommandWidget) Serve(c *Context) { var cmdUpdates *UpdateChan for u := range c.Input() { - if c.CurScreen() == "" && u.Message != nil { + if c.Path() == "" && u.Message != nil { // Skipping and executing the preinit action // while we have the empty screen. // E. g. the session did not start. diff --git a/tg/page.go b/tg/page.go index 811762c..bfd687c 100644 --- a/tg/page.go +++ b/tg/page.go @@ -15,12 +15,16 @@ type Page struct { } // Return new page with the specified text. -func NewPage(text string) *Page { +func NewPage() *Page { ret := &Page{} - ret.Text = text return ret } +func (p *Page) WithText(text string) *Page { + p.Text = text + return p +} + // Set the inline keyboard. func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page { p.Inline = inline diff --git a/tg/private.go b/tg/private.go index 501f5cc..8b25817 100644 --- a/tg/private.go +++ b/tg/private.go @@ -4,7 +4,7 @@ import ( "fmt" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "path" + //"path" ) type ContextType string @@ -25,7 +25,7 @@ type context struct { Bot *Bot skippedUpdates *UpdateChan // Current screen ID. - screenId, prevScreenId ScreenId + path, prevPath Path } // 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}) } -func (c *Context) CurScreen() ScreenId { - return c.screenId +func (c *Context) Path() Path { + return c.path } -func (c *Context) PrevScreen() ScreenId { - return c.prevScreenId +func (c *Context) PrevPath() Path { + return c.prevPath } func (c *Context) Run(a Action, u *Update) { 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 -type ScreenChange ScreenId +type ScreenChange Path func (sc ScreenChange) Act(c *Context) { - if !c.Bot.behaviour.ScreenExist(ScreenId(sc)) { + if !c.Bot.behaviour.PathExist(Path(sc)) { panic(ScreenNotExistErr) } - err := c.Go(ScreenId(sc)) + err := c.Go(Path(sc)) if err != nil { panic(err) } @@ -159,18 +159,22 @@ func (sc ScreenChange) Act(c *Context) { type C = Context // Changes screen of user to the Id one. -func (c *Context) Go(screenId ScreenId, args ...any) error { - if !c.Bot.behaviour.ScreenExist(screenId) { +func (c *Context) Go(pth Path, args ...any) error { + if !c.PathExist(pth) { return ScreenNotExistErr } // Getting the screen and changing to // then executing its widget. - screen := c.Bot.behaviour.Screens[screenId] - c.prevScreenId = c.screenId - c.screenId = screenId + if !pth.IsAbs() { + pth = (c.Path() + "/" + pth).Clean() + } + + c.prevPath = c.path + c.path = pth // Stopping the current widget. + screen := c.Bot.behaviour.Screens[pth] c.skippedUpdates.Close() if screen.Widget != nil { c.skippedUpdates = c.RunWidget(screen.Widget, args...) @@ -181,6 +185,10 @@ func (c *Context) Go(screenId ScreenId, args ...any) error { 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. func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan { if widget == nil { @@ -209,12 +217,18 @@ func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan { 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() { - c.Go(ScreenId(path.Dir(string(c.CurScreen())))) + c.Go(c.Path().Dir()) } // Change screen to the previous. // To get to the parent screen use GoUp. func (c *Context) GoPrev() { - c.Go(c.PrevScreen()) + c.Go(c.PrevPath()) } diff --git a/tg/screen.go b/tg/screen.go index 9684177..18114e0 100644 --- a/tg/screen.go +++ b/tg/screen.go @@ -1,60 +1,100 @@ package tg -// Unique identifier for the screen. -type ScreenId string +import ( + "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. // Mostly what buttons to show. type Screen struct { - // Unique identifer to change to the screen - // via Context.ChangeScreen method. - Id ScreenId // The widget to run when reaching the screen. Widget Widget } +// The first node with the "/" path. +type RootNode struct { + Screen *Screen + Subs []*Node +} + // The node is a simple way to represent // tree-like structured applications. type Node struct { + Path Path Screen *Screen Subs []*Node } -func NewNode(id ScreenId, widget Widget, subs ...*Node) *Node { - ret := &Node{} - ret.Screen = NewScreen(id, widget) +// Return new root node with the specified widget in the screen. +func NewRootNode(widget Widget, subs ...*Node) *RootNode { + ret := &RootNode{} + ret.Screen = NewScreen(widget) ret.Subs = subs 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) - id := n.Screen.Id - m[id] = n.Screen - n.Screen.Id = id - var root ScreenId - if id == "/" { - root = "" - } else { - root = id - } + var root Path = "/" + m[root] = n.Screen for _, sub := range n.Subs { - buf := sub.screenMap(root + "/") + buf := sub.ScreenMap(root) for k, v := range buf { + _, ok := m[k] + if ok { + panic("duplicate paths in node definition") + } m[k] = v } } return m } -func (n *Node) screenMap(root ScreenId) ScreenMap { +func (n *Node) ScreenMap(root Path) ScreenMap { m := make(ScreenMap) - id := root+ n.Screen.Id - m[id] = n.Screen - n.Screen.Id = id + pth := (root + n.Path).Clean() + m[pth] = n.Screen for _, sub := range n.Subs { - buf := sub.screenMap(id + "/") + buf := sub.ScreenMap((pth + "/").Clean()) for k, v := range buf { + _, ok := m[k] + if ok { + panic("duplicate paths in node definition") + } m[k] = v } } @@ -62,12 +102,11 @@ func (n *Node) screenMap(root ScreenId) ScreenMap { } // Map structure for the screens. -type ScreenMap map[ScreenId]*Screen +type ScreenMap map[Path] *Screen // Returns the new screen with specified name and widget. -func NewScreen(id ScreenId, widget Widget) *Screen { +func NewScreen(widget Widget) *Screen { return &Screen{ - Id: id, Widget: widget, } }