diff --git a/cmd/test/main.go b/cmd/test/main.go index 841caeb..f3c72df 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -26,7 +26,7 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget { return ret } -func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) { +func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) error { for u := range updates { if u.Message == nil { continue @@ -34,6 +34,7 @@ func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) { text := u.Message.Text c.Sendf("%s", w.Mutate(text)) } + return nil } func ExtractSessionData(c *tg.Context) *SessionData { @@ -173,47 +174,47 @@ var beh = tg.NewBehaviour(). ), ), ), -).WithCommands( - tg.NewCommand("start"). - Desc("start or restart the bot or move to the start screen"). - ActionFunc(func(c *tg.Context){ - c.ChangeScreen("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"). - ActionFunc(func(c *tg.Context) { - /*c.Sendf("Type some text:") - msg, err := c.ReadTextMessage() - if err != nil { - return - } - c.Sendf("You typed %q", msg)*/ - }), - 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.Value().(*BotData) - c.Sendf("My name is %q", bd.Name) - }), -) + ).WithCommands( + tg.NewCommand("start"). + Desc("start or restart the bot or move to the start screen"). + ActionFunc(func(c *tg.Context){ + c.ChangeScreen("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"). + ActionFunc(func(c *tg.Context) { + /*c.Sendf("Type some text:") + msg, err := c.ReadTextMessage() + if err != nil { + return + } + c.Sendf("You typed %q", msg)*/ + }), + 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) { }). WithCommands( tg.NewGroupCommand("hello").ActionFunc(func(c *tg.GC) { - c.Send("Hello, World!") + c.Sendf("Hello, World!") }), tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) { d := c.Session().Data.(*SessionData) @@ -231,11 +232,15 @@ func main() { bot = bot. WithBehaviour(beh). WithGroupBehaviour(gBeh). - WithValue(&BotData{ - Name: "Jay", - }). Debug(true) + bot.Data = &BotData{ + Name: "Jay", + } + log.Printf("Authorized on account %s", bot.Api.Self.UserName) - bot.Run() + err = bot.Run() + if err != nil { + panic(err) + } } diff --git a/tg/bot.go b/tg/bot.go index 03b1c03..6069dfb 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -2,6 +2,7 @@ package tg import ( "errors" + "sort" //"fmt" @@ -14,6 +15,8 @@ type User = tgbotapi.User // The wrapper around Telegram API. type Bot struct { + // Custom data value. + Data any Api *tgbotapi.BotAPI Me *User // Private bot behaviour. @@ -24,7 +27,6 @@ type Bot struct { channelBehaviour *ChannelBehaviour sessions SessionMap groupSessions GroupSessionMap - value any } @@ -40,19 +42,6 @@ func NewBot(token string) (*Bot, error) { }, nil } -// Set the custom global value for the bot, -// so it can be accessed from the callback -// functions. -func (bot *Bot) WithValue(v any) *Bot { - bot.value = v - return bot -} - -// Get the global bot value. -func (bot *Bot) Value() any { - return bot.value -} - func (bot *Bot) Debug(debug bool) *Bot { bot.Api.Debug = debug return bot @@ -127,6 +116,40 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot { return b } +// Setting the command on the user side. +func (bot *Bot) setCommands( + scope tgbotapi.BotCommandScope, + cmdMap map[CommandName] BotCommander, +) { + // First the private commands. + names := []string{} + for name := range cmdMap { + names = append(names, string(name)) + } + sort.Strings([]string(names)) + + cmds := []BotCommander{} + for _, name := range names { + cmds = append( + cmds, + cmdMap[CommandName(name)], + ) + } + + botCmds := []tgbotapi.BotCommand{} + for _, cmd := range cmds { + botCmds = append(botCmds, cmd.ToApi()) + } + + //tgbotapi.NewBotCommandScopeAllPrivateChats(), + cfg := tgbotapi.NewSetMyCommandsWithScope( + scope, + botCmds..., + ) + + bot.Api.Request(cfg) +} + // Run the bot with the Behaviour. func (bot *Bot) Run() error { if bot.behaviour == nil && @@ -139,12 +162,28 @@ func (bot *Bot) Run() error { 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) } if bot.groupBehaviour != nil { + commanders := make(map[CommandName] BotCommander) + for k, v := range bot.groupBehaviour.Commands { + commanders[k] = v + } + bot.setCommands( + tgbotapi.NewBotCommandScopeAllGroupChats(), + commanders, + ) chn := make(chan *Update) handles["group"] = chn handles["supergroup"] = chn diff --git a/tg/command.go b/tg/command.go index cc0a3f2..27d7ff9 100644 --- a/tg/command.go +++ b/tg/command.go @@ -3,10 +3,13 @@ package tg import ( //"flag" - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) -type Message = apix.Message +type BotCommander interface { + ToApi() tgbotapi.BotCommand +} +type Message = tgbotapi.Message type CommandName string type Command struct { @@ -31,6 +34,13 @@ func (c *Command) ActionFunc(af ActionFunc) *Command { return c.WithAction(af) } +func (c *Command) ToApi() tgbotapi.BotCommand { + ret := tgbotapi.BotCommand{} + ret.Command = string(c.Name) + ret.Description = c.Description + return ret +} + func (c *Command) Desc(desc string) *Command { c.Description = desc return c @@ -62,3 +72,10 @@ func (cmd *GroupCommand) Desc(desc string) *GroupCommand { cmd.Description = desc return cmd } + +func (c *GroupCommand) ToApi() tgbotapi.BotCommand { + ret := tgbotapi.BotCommand{} + ret.Command = string(c.Name) + ret.Description = c.Description + return ret +} diff --git a/tg/group.go b/tg/group.go index 54b61fe..47ade58 100644 --- a/tg/group.go +++ b/tg/group.go @@ -3,7 +3,7 @@ package tg import ( "fmt" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) // Customized actions for the group behaviour. @@ -56,7 +56,12 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) { cmdName := CommandName(msg.Command()) // Skipping the commands sent not to us. - atName := msg.CommandWithAt()[len(cmdName)+1:] + withAt := msg.CommandWithAt() + if len(cmdName) == len(withAt) { + continue + } + + atName := withAt[len(cmdName)+1:] if c.Bot.Me.UserName != atName { continue } @@ -74,16 +79,20 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) { } } -func (c *groupContext) Sendf(format string, v ...any) error { - return c.Send(fmt.Sprintf(format, v...)) +func (c *groupContext) Sendf( + format string, + v ...any, +) (*Message, error) { + msg, err := c.Send(NewMessage( + c.Session.Id, fmt.Sprintf(format, v...), + )) + if err != nil { + return nil, err + } + return msg, err } // Sends into the chat specified values converted to strings. -func (c *groupContext) Send(v ...any) error { - msg := tgbotapi.NewMessage( - c.Session.Id.ToApi(), - fmt.Sprint(v...), - ) - _, err := c.Bot.Api.Send(msg) - return err +func (c *groupContext) Send(v Sendable) (*Message, error) { + return c.Bot.Send(c.Session.Id, v) } diff --git a/tg/private.go b/tg/private.go index 4f72d32..5c6e97c 100644 --- a/tg/private.go +++ b/tg/private.go @@ -11,7 +11,7 @@ type context struct { // To reach the bot abilities inside callbacks. Bot *Bot widgetUpdates chan *Update - CurScreen, PrevScreen *Screen + curScreen, prevScreen *Screen } // The type represents way to interact with user in @@ -71,10 +71,7 @@ func (c *context) handleUpdateChan(updates chan *Update) { } func (c *context) run(a Action, u *Update) { - go a.Act(&Context{ - context: c, - Update: u, - }) + a.Act(&Context{context: c, Update: u}) } func (c *context) Render(v Renderable) ([]*Message, error) { @@ -149,8 +146,8 @@ 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.prevScreen = c.curScreen + c.curScreen = screen // Making the new channel for the widget. if c.widgetUpdates != nil { diff --git a/tg/screen.go b/tg/screen.go index a2ee8ca..496293a 100644 --- a/tg/screen.go +++ b/tg/screen.go @@ -6,10 +6,14 @@ type ScreenId string // 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 + // Needs implementation later. + Dynamic DynamicWidget } // Map structure for the screens. @@ -23,3 +27,8 @@ func NewScreen(id ScreenId, widget Widget) *Screen { } } +func (s *Screen) WithDynamic(dynamic DynamicWidget) *Screen { + s.Dynamic = dynamic + return s +} + diff --git a/tg/widget.go b/tg/widget.go index cb07b60..f7c3f46 100644 --- a/tg/widget.go +++ b/tg/widget.go @@ -12,7 +12,20 @@ type Widget interface { // widget MUST end its work. // Mostly made by looping over the // updates range. - Serve(*Context, chan *Update) + Serve(*Context, chan *Update) error +} + +// Implementing the interface provides +type DynamicWidget interface { + MakeWidget() Widget +} + +// The function that implements the Widget +// interface. +type WidgetFunc func(*Context, chan *Update) + +func (wf WidgetFunc) Serve(c *Context, updates chan *Update){ + wf(c, updates) } // The basic widget to provide keyboard functionality @@ -64,10 +77,10 @@ func (p *Page) WithSub(sub Widget) *Page { func (p *Page) Serve( c *Context, updates chan *Update, -) { +) error { msgs, err := c.Render(p) if err != nil { - panic(err) + return err } // The inline message is always returned @@ -128,7 +141,7 @@ func (p *Page) Serve( _, err := c.Bot.Api.Request(cb) if err != nil { - panic(err) + return err } kbd := p.Inline if kbd == nil { @@ -156,6 +169,7 @@ func (p *Page) Serve( c.run(act, u) } } + return nil } func (s *Page) Render(