From 44185e92af38790aaaabd87d35c0fc7f10de26be Mon Sep 17 00:00:00 2001 From: surdeus Date: Sat, 12 Aug 2023 14:35:33 +0300 Subject: [PATCH] Implemented basic command handling. --- cmd/test/main.go | 46 ++++++++++++++------ src/tx/beh.go | 46 +++++++++++++++++++- src/tx/bot.go | 106 ++++++++++++++++++++++++++++++---------------- src/tx/button.go | 16 ------- src/tx/command.go | 51 ++++++++++++++++++++++ src/tx/context.go | 70 +++++++++++++++++------------- src/tx/session.go | 28 ++++++++---- 7 files changed, 258 insertions(+), 105 deletions(-) create mode 100644 src/tx/command.go diff --git a/cmd/test/main.go b/cmd/test/main.go index 8cd0828..e82b650 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -8,6 +8,10 @@ import ( "github.com/mojosa-software/got/src/tx" ) +type UserData struct { + Counter int +} + var startScreenButton = tx.NewButton(). WithText("🏠 To the start screen"). ScreenChange("start") @@ -17,21 +21,21 @@ var beh = tx.NewBehaviour(). // The function will be called every time // the bot is started. OnStartFunc(func(c *tx.Context) { - c.V["counter"] = new(int) + c.V = &UserData{} c.ChangeScreen("start") }).WithKeyboards( // Increment/decrement keyboard. tx.NewKeyboard("inc/dec").Row( tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) { - counter := c.V["counter"].(*int) - *counter++ - c.Sendf("%d", *counter) + d := c.V.(*UserData) + d.Counter++ + c.Sendf("%d", d.Counter) }), tx.NewButton().WithText("-").ActionFunc(func(c *tx.Context) { - counter := c.V["counter"].(*int) - *counter-- - c.Sendf("%d", *counter) + d := c.V.(*UserData) + d.Counter-- + c.Sendf("%d", d.Counter) }), ).Row( startScreenButton, @@ -67,16 +71,16 @@ var beh = tx.NewBehaviour(). tx.NewScreen("inc/dec"). WithText( - "The screen shows how"+ - "user separated data works"+ - "by saving the counter for each of users"+ - "separately.", + "The screen shows how "+ + "user separated data works "+ + "by saving the counter for each of users "+ + "separately. ", ). Keyboard("inc/dec"). // The function will be called when reaching the screen. ActionFunc(func(c *tx.Context) { - counter := c.V["counter"].(*int) - c.Sendf("Current counter value = %d", *counter) + d := c.V.(*UserData) + c.Sendf("Current counter value = %d", d.Counter) }), tx.NewScreen("upper-case"). @@ -88,6 +92,22 @@ var beh = tx.NewBehaviour(). WithText("Type text and the bot will send you the lower case version"). Keyboard("nav-start"). ActionFunc(mutateMessage(strings.ToLower)), +).WithCommands( + tx.NewCommand("hello"). + Desc("sends the 'Hello, World!' message back"). + ActionFunc(func(c *tx.Context) { + c.Send("Hello, World!") + }), + tx.NewCommand("read"). + Desc("reads a string and sends it back"). + ActionFunc(func(c *tx.Context) { + c.Send("Type some text:") + msg, err := c.ReadTextMessage() + if err != nil { + return + } + c.Sendf("You typed %q", msg) + }), ) func mutateMessage(fn func(string) string) tx.ActionFunc { diff --git a/src/tx/beh.go b/src/tx/beh.go index c431156..f96297f 100644 --- a/src/tx/beh.go +++ b/src/tx/beh.go @@ -3,11 +3,24 @@ package tx // The package implements // behaviour for the Telegram bots. -// The type describes behaviour for the bot. +// The type describes behaviour for the bot in personal chats. type Behaviour struct { Start Action Screens ScreenMap Keyboards KeyboardMap + Commands CommandMap +} + +// The type describes behaviour for the bot in group chats. +type GroupBehaviour struct { + // Will be called on adding the bot to the group. + //Add GroupAction + // List of commands + Commands CommandMap +} + +// The type describes behaviour for the bot in channels. +type ChannelBehaviour struct { } // Returns new empty behaviour. @@ -15,6 +28,7 @@ func NewBehaviour() *Behaviour { return &Behaviour{ Screens: make(ScreenMap), Keyboards: make(KeyboardMap), + Commands: make(CommandMap), } } @@ -69,6 +83,36 @@ func (b *Behaviour) WithScreens( return b } +// The function sets 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 + } + return b +} + +// The function sets group commands. +/*func (b *Behaviour) WithGroupCommands(cmds ...*Command) *Behaviour { + for _, cmd := range cmds { + if cmd.Name == "" { + panic("empty group command name") + } + _, ok := b.GroupCommands[cmd.Name] + if ok { + panic("duplicate group command definition") + } + b.GroupCommands[cmd.Name] = cmd + } + return b +}*/ + // Check whether the screen exists in the behaviour. func (beh *Behaviour) ScreenExist(id ScreenId) bool { _, ok := beh.Screens[id] diff --git a/src/tx/bot.go b/src/tx/bot.go index c1d8b86..8def42d 100644 --- a/src/tx/bot.go +++ b/src/tx/bot.go @@ -1,8 +1,9 @@ package tx import ( + "fmt" + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" - //"log" ) // The wrapper around Telegram API. @@ -34,51 +35,82 @@ func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) { // Run the bot with the Behaviour. func (bot *Bot) Run() error { bot.Debug = true - uc := apix.NewUpdate(0) uc.Timeout = 60 - updates := bot.GetUpdatesChan(uc) - - chans := make(map[SessionId]chan *Update) + privateChans := make(map[SessionId]chan *Update) + groupChans := make(map[SessionId]chan *Update) for u := range updates { - var sid SessionId + var chatType string + if u.Message != nil { - // Create new session if the one does not exist - // for this user. - sid = SessionId(u.Message.Chat.ID) - if _, ok := bot.sessions[sid]; !ok { - bot.sessions.Add(sid) - } - - // The "start" command resets the bot - // by executing the Start Action. - if u.Message.IsCommand() { - cmd := u.Message.Command() - if cmd == "start" { - // Getting current session and context. - session := bot.sessions[sid] - ctx := &Context{ - B: bot, - Session: session, - updates: make(chan *Update), - } - - chn := make(chan *Update) - chans[sid] = chn - // Starting the goroutine for the user. - go ctx.handleUpdateChan(chn) - continue - } - } + chatType = u.Message.Chat.Type } else if u.CallbackQuery != nil { - sid = SessionId(u.CallbackQuery.Message.Chat.ID) + chatType = u.Message.Chat.Type } - chn, ok := chans[sid] - if ok { - chn <- &u + + switch chatType { + case "private": + bot.handlePrivate(&u, privateChans) + case "group", "supergroup": + bot.handleGroup(&u, groupChans) } } return nil } + +// The function handles updates supposed for the private +// chat with the bot. +func (bot *Bot) handlePrivate(u *Update, chans map[SessionId]chan *Update) { + var sid SessionId + if u.Message != nil { + msg := u.Message + + if bot.Debug { + fmt.Printf("is command: %q\n", u.Message.IsCommand()) + fmt.Printf("command itself: %q\n", msg.Command()) + fmt.Printf("command arguments: %q\n", msg.CommandArguments()) + fmt.Printf("is to me: %q\n", bot.IsMessageToMe(*msg)) + } + + // Create new session if the one does not exist + // for this user. + sid = SessionId(u.Message.Chat.ID) + if _, ok := bot.sessions[sid]; !ok { + bot.sessions.Add(sid) + } + + // The "start" command resets the bot + // by executing the Start Action. + if u.Message.IsCommand() { + cmdName := CommandName(u.Message.Command()) + if cmdName == "start" { + // Getting current session and context. + session := bot.sessions[sid] + ctx := &Context{ + B: bot, + Session: session, + updates: make(chan *Update), + } + + chn := make(chan *Update) + chans[sid] = chn + // Starting the goroutine for the user. + go ctx.handleUpdateChan(chn) + } + } + } else if u.CallbackQuery != nil { + sid = SessionId(u.CallbackQuery.Message.Chat.ID) + } + chn, ok := chans[sid] + // The bot MUST get the "start" command. + // It will do nothing otherwise. + if ok { + chn <- u + } +} + +// Not implemented yet. +func (bot *Bot) handleGroup(u *Update, chans map[SessionId]chan *Update) { +} diff --git a/src/tx/button.go b/src/tx/button.go index e939b8e..1fae13e 100644 --- a/src/tx/button.go +++ b/src/tx/button.go @@ -45,22 +45,6 @@ func (btn *Button) ScreenChange(sc ScreenChange) *Button { return btn.WithAction(sc) } -func NewButtonData(text string, data string, action Action) *Button { - return &Button{ - Text: text, - Data: data, - Action: action, - } -} - -func NewButtonUrl(text string, url string, action Action) *Button { - return &Button{ - Text: text, - Url: url, - Action: action, - } -} - func (btn *Button) ToTelegram() apix.KeyboardButton { return apix.NewKeyboardButton(btn.Text) } diff --git a/src/tx/command.go b/src/tx/command.go new file mode 100644 index 0000000..58247c7 --- /dev/null +++ b/src/tx/command.go @@ -0,0 +1,51 @@ +package tx + +import ( + //"flag" + + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type Message = apix.Message + +type CommandName string + +type CommandContext struct { + // The field declares way to interact with the group chat in + // general. + *Context + Message *Message +} + +type CommandMap map[CommandName]*Command + +type CommandHandlerFunc func(*CommandContext) +type CommandHandler interface { + Run(*Context) +} + +type Command struct { + Name CommandName + Description string + Action Action +} + +func NewCommand(name CommandName) *Command { + return &Command{ + Name: name, + } +} + +func (c *Command) WithAction(a Action) *Command { + c.Action = a + return c +} + +func (c *Command) ActionFunc(af ActionFunc) *Command { + return c.WithAction(af) +} + +func (c *Command) Desc(desc string) *Command { + c.Description = desc + return c +} diff --git a/src/tx/context.go b/src/tx/context.go index b1c0b91..04bce78 100644 --- a/src/tx/context.go +++ b/src/tx/context.go @@ -10,10 +10,17 @@ import ( // handling functions. Is provided to Act() function always. type Context struct { *Session - B *Bot - updates chan *Update - available bool - availableChan chan bool + B *Bot + updates chan *Update + + // Is true if currently reading the Update. + readingUpdate bool +} + +// Context for interaction inside groups. +type GroupContext struct { + *GroupSession + B *Bot } // Goroutie function to handle each user. @@ -25,28 +32,33 @@ func (c *Context) handleUpdateChan(updates chan *Update) { screen := bot.Screens[session.CurrentScreenId] // The part is added to implement custom update handling. if u.Message != nil { + var act Action + if u.Message.IsCommand() && !c.readingUpdate { + cmdName := CommandName(u.Message.Command()) + cmd, ok := bot.Behaviour.Commands[cmdName] + if ok { + act = cmd.Action + } else { + } + } else { + kbd := bot.Keyboards[screen.KeyboardId] + btns := kbd.buttonMap() + text := u.Message.Text + btn, ok := btns[text] + // Sending wrong messages to + // the currently reading goroutine. + if !ok && c.readingUpdate { + c.updates <- u + continue + } - kbd := bot.Keyboards[screen.KeyboardId] - btns := kbd.buttonMap() - text := u.Message.Text - btn, ok := btns[text] - - /*if ok { - c.available = false - btn.Action.Act(c) - c.available = true - continue - }*/ - - // Sending wrong messages to - // the currently reading goroutine. - if !ok && c.readingUpdate { - c.updates <- u - continue + if ok { + act = btn.Action + } } - if ok && btn.Action != nil { - c.run(btn.Action) + if act != nil { + c.run(act) } } else if u.CallbackQuery != nil { cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data) @@ -68,10 +80,6 @@ func (c *Context) run(a Action) { go a.Act(c) } -func (c *Context) Available() bool { - return c.available -} - // Changes screen of user to the Id one. func (c *Context) ChangeScreen(screenId ScreenId) error { // Return if it will not change anything. @@ -83,15 +91,17 @@ func (c *Context) ChangeScreen(screenId ScreenId) error { return ScreenNotExistErr } + // Stop the reading by sending the nil. + if c.readingUpdate { + c.updates <- nil + } + screen := c.B.Screens[screenId] screen.Render(c) c.Session.ChangeScreen(screenId) c.KeyboardId = screen.KeyboardId - if c.readingUpdate { - c.updates <- nil - } if screen.Action != nil { c.run(screen.Action) } diff --git a/src/tx/session.go b/src/tx/session.go index ee83bfa..f15fff3 100644 --- a/src/tx/session.go +++ b/src/tx/session.go @@ -13,7 +13,6 @@ type SessionId int64 // The type represents current state of // user interaction per each of them. type Session struct { - // Unique identifier for the session, Telegram chat's ID. Id SessionId // Current screen identifier. CurrentScreenId ScreenId @@ -21,19 +20,24 @@ type Session struct { PreviousScreenId ScreenId // The currently showed on display keyboard inside Action. KeyboardId KeyboardId - - // Is true if currently reading the Update. - readingUpdate bool - - // Custom data for each user. - V map[string]any + V any } // The type represents map of sessions using // as key. type SessionMap map[SessionId]*Session -// Return new empty session with +// Session information for a group. +type GroupSession struct { + Id SessionId + // Information for each user in the group. + V map[SessionId]any +} + +// Map for every user in every chat sessions. +type GroupSessionMap map[SessionId]*GroupSession + +// Return new empty session with specified user ID. func NewSession(id SessionId) *Session { return &Session{ Id: id, @@ -41,6 +45,14 @@ func NewSession(id SessionId) *Session { } } +// Returns new empty group session with specified group and user IDs. +func NewGroupSession(id SessionId) *GroupSession { + return &GroupSession{ + Id: id, + V: make(map[SessionId]any), + } +} + // Changes screen of user to the Id one for the session. func (c *Session) ChangeScreen(screenId ScreenId) { c.PreviousScreenId = c.CurrentScreenId