package tg import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) type CommandType uint8 const ( PrivateCommandType CommandType = iota GroupCommandType ChannelCommandType ) type CommandName string type Command struct { Name CommandName Type CommandType Description string Action Action Widget Widget WidgetArg any } type CommandMap map[CommandName]Command func NewCommand(name CommandName, desc string) Command { if name == "" || desc == "" { panic("name and description cannot be an empty string") } return Command{ Name: name, Description: desc, } } func (c Command) WithAction(a Action) Command { c.Action = a return c } func (c Command) WithWidget(w Widget) Command { c.Widget = w return c } // Convert command into the tgbotapi.BotCommand func (c Command) ToAPI() tgbotapi.BotCommand { ret := tgbotapi.BotCommand{} ret.Command = string(c.Name) ret.Description = c.Description return ret } // Simple command to go to another screen. func (c Command) Go(pth Widget) Command { return c.WithAction(WidgetGo{ Path: pth, }) } func (c Command) GoWithArg(pth Widget, arg any) Command { return c.WithAction(WidgetGo{ Path: pth, Arg: arg, }) } // The type is used to recognize commands and execute // its actions and widgets . type CommandCompo struct { PreStart Action Commands CommandMap Usage Action } // Returns new empty CommandCompo. func NewCommandCompo(cmds ...Command) *CommandCompo { ret := (&CommandCompo{}).SetCommands(cmds...) return ret } // Set the commands to handle. func (w *CommandCompo) SetCommands(cmds ...Command) *CommandCompo { if w.Commands == nil { w.Commands = make(CommandMap) } 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 *CommandCompo) SetPreStart(a Action) *CommandCompo { w.PreStart = a return w } // Set the usage action. func (w *CommandCompo) SetUsage(a Action) *CommandCompo { w.Usage = a return w } // Filtering all the non commands. func (widget *CommandCompo) Filter( u Update, ) bool { if u.Message == nil || !u.Message.IsCommand() { return false } return false } // Implementing server. func (compo *CommandCompo) Serve(c Context) { // First should bring the new command into the action. c.Bot().DeleteCommands() err := c.Bot().SetCommands( tgbotapi.NewBotCommandScopeChat(c.SessionID().ToAPI()), compo.Commands, ) if err != nil { c.Sendf("error: %q", err) } var cmdUpdates *UpdateChan for u := range c.Input() { if c.Path() == nil && 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.WithUpdate(u).Run(compo.PreStart) continue } } if u.Message != nil && u.Message.IsCommand() { // Command handling. cmdName := CommandName(u.Message.Command()) cmd, ok := compo.Commands[cmdName] if !ok { c.WithUpdate(u).Run(compo.Usage) continue } c.WithUpdate(u).Run(cmd.Action) if cmd.Widget != nil { // Closing current widget cmdUpdates.Close() // And running the other one. cmdUpdates, _ = c.WithArg(cmd.WidgetArg).RunWidget(cmd.Widget) } continue } if !cmdUpdates.Closed() { // Send to the commands channel if we are // executing one. cmdUpdates.Send(u) } else { c.SkipUpdate(u) } } }