diff --git a/cmd/test/main.go b/cmd/test/main.go index f3c72df..4d07e3a 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -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"). diff --git a/tg/beh.go b/tg/beh.go index 29119ec..633cf1d 100644 --- a/tg/beh.go +++ b/tg/beh.go @@ -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 } diff --git a/tg/bot.go b/tg/bot.go index 6069dfb..2fe226d 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -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, ) diff --git a/tg/command.go b/tg/command.go index c333a16..3d573b5 100644 --- a/tg/command.go +++ b/tg/command.go @@ -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 +} diff --git a/tg/page.go b/tg/page.go index fed97d9..d584edc 100644 --- a/tg/page.go +++ b/tg/page.go @@ -141,9 +141,7 @@ func (p *Page) Serve( act = kbd.Action } } - if act != nil { - c.run(act, u) - } + c.Run(act, u) } return nil } diff --git a/tg/private.go b/tg/private.go index 1f05f6e..1fbf77f 100644 --- a/tg/private.go +++ b/tg/private.go @@ -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 }