Added the Node notation to define paths to screens..
This commit is contained in:
parent
c96c2a7559
commit
061add76a8
7 changed files with 199 additions and 158 deletions
184
cmd/test/main.go
184
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)
|
||||
|
|
20
tg/beh.go
20
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
93
tg/screen.go
93
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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue