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.
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(
tg.NewScreen("start", tg.NewPage(
"The bot started!",
@ -187,13 +184,17 @@ var beh = tg.NewBehaviour().
}),
tg.NewCommand("read").
Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) {
/*c.Sendf("Type some text:")
msg, err := c.ReadTextMessage()
if err != nil {
return
WidgetFunc(func(c *tg.Context, updates chan *tg.Update) error {
c.Sendf("Type text and I will send it back to you")
for u := range updates {
if u.Message == nil {
continue
}
c.Sendf("You typed %q", u.Message.Text)
break
}
c.Sendf("You typed %q", msg)*/
c.Sendf("Done")
return nil
}),
tg.NewCommand("image").
Desc("sends a sample image").

View file

@ -5,20 +5,24 @@ package tg
// The type describes behaviour for the bot in personal chats.
type Behaviour struct {
PreStart *action
Root Widget
Init *action
Screens ScreenMap
Commands CommandMap
}
// Returns new empty behaviour.
func NewBehaviour() *Behaviour {
return &Behaviour{
Screens: make(ScreenMap),
Commands: make(CommandMap),
Screens: make(ScreenMap),
}
}
// 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,
// not when starting or restarting the bot with the Start Action.
func (b *Behaviour) WithInit(a Action) *Behaviour {
@ -33,20 +37,6 @@ func (b *Behaviour) WithInitFunc(
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.
func (b *Behaviour) WithScreens(
screens ...*Screen,
@ -64,18 +54,17 @@ func (b *Behaviour) WithScreens(
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 {
for _, cmd := range cmds {
if cmd.Name == "" {
panic("empty command name")
}
_, ok := b.Commands[cmd.Name]
if ok {
panic("duplicate command definition")
}
b.Commands[cmd.Name] = cmd
}
b.Root = NewCommandWidget().
WithCommands(cmds...).
WithPreStartFunc(func(c *Context){
c.Sendf("Please, use the /start command to start")
}).WithUsageFunc(func(c *Context){
c.Sendf("No such command")
})
return b
}

View file

@ -117,7 +117,7 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot {
}
// Setting the command on the user side.
func (bot *Bot) setCommands(
func (bot *Bot) SetCommands(
scope tgbotapi.BotCommandScope,
cmdMap map[CommandName] BotCommander,
) {
@ -156,20 +156,17 @@ func (bot *Bot) Run() error {
bot.groupBehaviour == nil {
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.Timeout = 60
updates := bot.Api.GetUpdatesChan(uc)
handles := make(map[string]chan *Update)
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)
handles["private"] = chn
go bot.handlePrivate(chn)
@ -180,7 +177,7 @@ func (bot *Bot) Run() error {
for k, v := range bot.groupBehaviour.Commands {
commanders[k] = v
}
bot.setCommands(
bot.SetCommands(
tgbotapi.NewBotCommandScopeAllGroupChats(),
commanders,
)

View file

@ -89,3 +89,113 @@ func (c *GroupCommand) ToApi() tgbotapi.BotCommand {
ret.Description = c.Description
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
}
}
if act != nil {
c.run(act, u)
}
c.Run(act, u)
}
return nil
}

View file

@ -10,100 +10,67 @@ type context struct {
Session *Session
// To reach the bot abilities inside callbacks.
Bot *Bot
widgetUpdates chan *Update
curScreen, prevScreen *Screen
skippedUpdates chan *Update
// Current screen ID.
screenId, prevScreenId ScreenId
}
// The type represents way to interact with user in
// handling functions. Is provided to Act() function always.
// Goroutie function to handle each user.
func (c *context) handleUpdateChan(updates chan *Update) {
beh := c.Bot.behaviour
session := c.Session
preStart := beh.PreStart
if beh.Init != nil {
c.run(beh.Init, nil)
}
var cmdUpdates chan *Update
for u := range updates {
// The part is added to implement custom update handling.
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
}
}
beh.Root.Serve(&Context{
context: c,
}, updates)
}
func (c *context) run(a Action, u *Update) {
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)
}
// 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)
}
// 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(
c.Session.Id, fmt.Sprintf(format, v...),
))
@ -165,29 +132,22 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
// Getting the screen and changing to
// then executing its widget.
screen := c.Bot.behaviour.Screens[screenId]
c.prevScreen = c.curScreen
c.curScreen = screen
c.prevScreenId = c.screenId
c.screenId = screenId
// Making the new channel for the widget.
if c.widgetUpdates != nil {
close(c.widgetUpdates)
if c.skippedUpdates != nil {
close(c.skippedUpdates)
}
c.widgetUpdates = make(chan *Update)
c.skippedUpdates = make(chan *Update)
if screen.Widget != nil {
// 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() {
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
}