Made the root Widget a separate entity.

This commit is contained in:
Andrey Parhomenko 2023-09-11 15:58:45 +03:00
parent 36fa167549
commit 1d543bf444
6 changed files with 190 additions and 135 deletions

View file

@ -110,9 +110,6 @@ var beh = tg.NewBehaviour().
// The session initialization. // The session initialization.
c.Session.Data = &SessionData{} c.Session.Data = &SessionData{}
}). // On any message update before the bot created session.
WithPreStartFunc(func(c *tg.Context){
c.Sendf("Please, use the /start command to start the bot")
}).WithScreens( }).WithScreens(
tg.NewScreen("start", tg.NewPage( tg.NewScreen("start", tg.NewPage(
"The bot started!", "The bot started!",
@ -187,13 +184,17 @@ var beh = tg.NewBehaviour().
}), }),
tg.NewCommand("read"). tg.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) { WidgetFunc(func(c *tg.Context, updates chan *tg.Update) error {
/*c.Sendf("Type some text:") c.Sendf("Type text and I will send it back to you")
msg, err := c.ReadTextMessage() for u := range updates {
if err != nil { if u.Message == nil {
return continue
}
c.Sendf("You typed %q", u.Message.Text)
break
} }
c.Sendf("You typed %q", msg)*/ c.Sendf("Done")
return nil
}), }),
tg.NewCommand("image"). tg.NewCommand("image").
Desc("sends a sample image"). Desc("sends a sample image").

View file

@ -5,20 +5,24 @@ package tg
// 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 {
PreStart *action Root Widget
Init *action Init *action
Screens ScreenMap Screens ScreenMap
Commands CommandMap
} }
// Returns new empty behaviour. // Returns new empty behaviour.
func NewBehaviour() *Behaviour { func NewBehaviour() *Behaviour {
return &Behaviour{ return &Behaviour{
Screens: make(ScreenMap), Screens: make(ScreenMap),
Commands: make(CommandMap),
} }
} }
// Set the root widget. Mostly the CommandWidget is used.
func (b *Behaviour) WithRoot(root Widget) *Behaviour {
b.Root = root
return b
}
// The Action will be called on session creation, // The Action will be called on session creation,
// not when starting or restarting the bot with the Start Action. // not when starting or restarting the bot with the Start Action.
func (b *Behaviour) WithInit(a Action) *Behaviour { func (b *Behaviour) WithInit(a Action) *Behaviour {
@ -33,20 +37,6 @@ func (b *Behaviour) WithInitFunc(
return b.WithInit(fn) return b.WithInit(fn)
} }
// Defines pre-start action.
// E. g. when the user has not type the "/start" command.
// Mostly used to send the "/start" command back
// with some warning.
func (b *Behaviour) WithPreStart(a Action) *Behaviour {
b.PreStart = newAction(a)
return b
}
// Alias for WithPreStart to be used with function inside.
func (b *Behaviour) WithPreStartFunc(fn ActionFunc) *Behaviour {
return b.WithPreStart(fn)
}
// The function sets screens. // The function sets screens.
func (b *Behaviour) WithScreens( func (b *Behaviour) WithScreens(
screens ...*Screen, screens ...*Screen,
@ -64,18 +54,17 @@ func (b *Behaviour) WithScreens(
return b return b
} }
// The function sets commands. // The function sets as the standard root widget CommandWidget
// and its commands..
func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour { func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour {
for _, cmd := range cmds { b.Root = NewCommandWidget().
if cmd.Name == "" { WithCommands(cmds...).
panic("empty command name") WithPreStartFunc(func(c *Context){
} c.Sendf("Please, use the /start command to start")
_, ok := b.Commands[cmd.Name] }).WithUsageFunc(func(c *Context){
if ok { c.Sendf("No such command")
panic("duplicate command definition") })
}
b.Commands[cmd.Name] = cmd
}
return b return b
} }

View file

@ -117,7 +117,7 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot {
} }
// Setting the command on the user side. // Setting the command on the user side.
func (bot *Bot) setCommands( func (bot *Bot) SetCommands(
scope tgbotapi.BotCommandScope, scope tgbotapi.BotCommandScope,
cmdMap map[CommandName] BotCommander, cmdMap map[CommandName] BotCommander,
) { ) {
@ -156,20 +156,17 @@ func (bot *Bot) Run() error {
bot.groupBehaviour == nil { bot.groupBehaviour == nil {
return errors.New("no behaviour defined") return errors.New("no behaviour defined")
} }
if bot.behaviour != nil && bot.behaviour.Root == nil {
return errors.New("the root widget is not set, cannot run")
}
uc := tgbotapi.NewUpdate(0) uc := tgbotapi.NewUpdate(0)
uc.Timeout = 60 uc.Timeout = 60
updates := bot.Api.GetUpdatesChan(uc) updates := bot.Api.GetUpdatesChan(uc)
handles := make(map[string]chan *Update) handles := make(map[string]chan *Update)
if bot.behaviour != nil { if bot.behaviour != nil {
commanders := make(map[CommandName] BotCommander)
for k, v := range bot.behaviour.Commands {
commanders[k] = v
}
bot.setCommands(
tgbotapi.NewBotCommandScopeAllPrivateChats(),
commanders,
)
chn := make(chan *Update) chn := make(chan *Update)
handles["private"] = chn handles["private"] = chn
go bot.handlePrivate(chn) go bot.handlePrivate(chn)
@ -180,7 +177,7 @@ func (bot *Bot) Run() error {
for k, v := range bot.groupBehaviour.Commands { for k, v := range bot.groupBehaviour.Commands {
commanders[k] = v commanders[k] = v
} }
bot.setCommands( bot.SetCommands(
tgbotapi.NewBotCommandScopeAllGroupChats(), tgbotapi.NewBotCommandScopeAllGroupChats(),
commanders, commanders,
) )

View file

@ -89,3 +89,113 @@ func (c *GroupCommand) ToApi() tgbotapi.BotCommand {
ret.Description = c.Description ret.Description = c.Description
return ret return ret
} }
// The type is used to recognize commands and execute
// its actions and widgets .
type CommandWidget struct {
PreStart Action
Commands CommandMap
Usage Action
}
// Returns new empty CommandWidget.
func NewCommandWidget() *CommandWidget {
ret := &CommandWidget{}
ret.Commands = make(CommandMap)
return ret
}
// Set the commands to handle.
func (w *CommandWidget) WithCommands(cmds ...*Command) *CommandWidget {
for _, cmd := range cmds {
if cmd.Name == "" {
panic("empty command name")
}
_, ok := w.Commands[cmd.Name]
if ok {
panic("duplicate command definition")
}
w.Commands[cmd.Name] = cmd
}
return w
}
// Set the prestart action.
func (w *CommandWidget) WithPreStart(a Action) *CommandWidget {
w.PreStart = a
return w
}
// Set the prestart action with function.
func (w *CommandWidget) WithPreStartFunc(fn ActionFunc) *CommandWidget {
return w.WithPreStart(fn)
}
// Set the usage action.
func (w *CommandWidget) WithUsage(a Action) *CommandWidget {
w.Usage = a
return w
}
// Set the usage action with function.
func (w *CommandWidget) WithUsageFunc(fn ActionFunc) *CommandWidget {
return w.WithUsage(fn)
}
func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
commanders := make(map[CommandName] BotCommander)
for k, v := range widget.Commands {
commanders[k] = v
}
c.Bot.SetCommands(
tgbotapi.NewBotCommandScopeAllPrivateChats(),
commanders,
)
var cmdUpdates chan *Update
for u := range updates {
if c.ScreenId() == "" && u.Message != nil {
// Skipping and executing the preinit action
// while we have the empty screen.
// E. g. the session did not start.
if !(u.Message.IsCommand() && u.Message.Command() == "start") {
c.Run(widget.PreStart, u)
continue
}
}
if u.Message != nil && u.Message.IsCommand() {
// Command handling.
cmdName := CommandName(u.Message.Command())
cmd, ok := widget.Commands[cmdName]
if !ok {
c.Run(widget.Usage, u)
continue
}
c.Run(cmd.Action, u)
if cmd.Widget != nil {
if cmdUpdates != nil {
close(cmdUpdates)
}
cmdUpdates = make(chan *Update)
go func() {
cmd.Widget.Serve(
&Context{context: c.context, Update: u},
cmdUpdates,
)
close(cmdUpdates)
cmdUpdates = nil
}()
}
}
if cmdUpdates != nil {
// Send to the commands channel if we are
// executing one.
cmdUpdates <- u
} else {
c.Skip(u)
}
}
return nil
}

View file

@ -141,9 +141,7 @@ func (p *Page) Serve(
act = kbd.Action act = kbd.Action
} }
} }
if act != nil { c.Run(act, u)
c.run(act, u)
}
} }
return nil return nil
} }

View file

@ -10,100 +10,67 @@ type context struct {
Session *Session Session *Session
// To reach the bot abilities inside callbacks. // To reach the bot abilities inside callbacks.
Bot *Bot Bot *Bot
widgetUpdates chan *Update skippedUpdates chan *Update
curScreen, prevScreen *Screen // Current screen ID.
screenId, prevScreenId ScreenId
} }
// The type represents way to interact with user in // The type represents way to interact with user in
// handling functions. Is provided to Act() function always. // handling functions. Is provided to Act() function always.
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *context) handleUpdateChan(updates chan *Update) { func (c *context) handleUpdateChan(updates chan *Update) {
beh := c.Bot.behaviour beh := c.Bot.behaviour
session := c.Session
preStart := beh.PreStart
if beh.Init != nil { if beh.Init != nil {
c.run(beh.Init, nil) c.run(beh.Init, nil)
} }
var cmdUpdates chan *Update beh.Root.Serve(&Context{
for u := range updates { context: c,
// The part is added to implement custom update handling. }, updates)
if !session.started {
if u.Message.IsCommand() &&
u.Message.Command() == "start" {
// Special treatment for the "/start"
// command.
session.started = true
cmdName := CommandName("start")
cmd, ok := beh.Commands[cmdName]
if ok {
if cmd.Action != nil {
c.run(cmd.Action, u)
}
} else {
// Some usage.
}
} else {
// Prestart handling.
c.run(preStart, u)
}
continue
}
if u.Message != nil && u.Message.IsCommand() {
// Command handling.
cmdName := CommandName(u.Message.Command())
cmd, ok := beh.Commands[cmdName]
if ok {
if cmd.Action != nil {
c.run(cmd.Action, u)
}
if cmd.Widget != nil {
if cmdUpdates != nil {
close(cmdUpdates)
}
cmdUpdates = make(chan *Update)
go func() {
cmd.Widget.Serve(
&Context{context: c, Update: u},
cmdUpdates,
)
close(cmdUpdates)
cmdUpdates = nil
}()
}
} else {
// Some usage.
}
continue
}
// The standard thing - send messages to widgets.
if cmdUpdates != nil {
cmdUpdates <- u
} else {
c.widgetUpdates <- u
}
}
} }
func (c *context) run(a Action, u *Update) { 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) Render(v Renderable) ([]*Message, error) { func (c *Context) ScreenId() ScreenId {
return c.screenId
}
func (c *Context) PrevScreenId() ScreenId {
return c.prevScreenId
}
func (c *Context) Run(a Action, u *Update) {
if a != nil {
a.Act(&Context{context: c.context, Update: u})
}
}
// Only for the root widget usage.
// Skip the update sending it down to
// the underlying widget.
func (c *Context) Skip(u *Update) {
if c.skippedUpdates != nil {
c.skippedUpdates <- u
}
}
// Renders the Renedrable object to the side of client
// and returns the messages it sent.
func (c *Context) Render(v Renderable) ([]*Message, error) {
return c.Bot.Render(c.Session.Id, v) return c.Bot.Render(c.Session.Id, v)
} }
// Sends to the Sendable object. // Sends to the Sendable object.
func (c *context) Send(v Sendable) (*Message, error) { func (c *Context) Send(v Sendable) (*Message, error) {
return c.Bot.Send(c.Session.Id, v) return c.Bot.Send(c.Session.Id, v)
} }
// Sends the formatted with fmt.Sprintf message to the user. // Sends the formatted with fmt.Sprintf message to the user.
func (c *context) Sendf(format string, v ...any) (*Message, error) { func (c *Context) Sendf(format string, v ...any) (*Message, error) {
msg, err := c.Send(NewMessage( msg, err := c.Send(NewMessage(
c.Session.Id, fmt.Sprintf(format, v...), c.Session.Id, fmt.Sprintf(format, v...),
)) ))
@ -165,29 +132,22 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
// 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] screen := c.Bot.behaviour.Screens[screenId]
c.prevScreen = c.curScreen c.prevScreenId = c.screenId
c.curScreen = screen c.screenId = screenId
// Making the new channel for the widget. // Making the new channel for the widget.
if c.widgetUpdates != nil { if c.skippedUpdates != nil {
close(c.widgetUpdates) close(c.skippedUpdates)
} }
c.widgetUpdates = make(chan *Update) c.skippedUpdates = make(chan *Update)
if screen.Widget != nil { if screen.Widget != nil {
// Running the widget if the screen has one. // Running the widget if the screen has one.
go screen.Widget.Serve(c, c.widgetUpdates)
} else {
// Skipping updates if there is no
// widget to handle them.
go func() { go func() {
for _ = range c.widgetUpdates {} screen.Widget.Serve(c, c.skippedUpdates)
}() }()
} else {
panic("no widget defined for the screen")
} }
//c.Bot.Render(c.Session.Id, screen)
//if screen.Action != nil {
//c.run(screen.Action, c.Update)
//}
return nil return nil
} }