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 (
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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