From c2562cc54c0a93c8618724b28a6dd71eae9807ce Mon Sep 17 00:00:00 2001 From: surdeus Date: Sun, 13 Aug 2023 15:37:36 +0300 Subject: [PATCH] Implemented basic group behaviour. --- cmd/test/main.go | 18 +++- src/tx/action.go | 110 ++++++++++++++++++------ src/tx/beh.go | 54 +++++++++--- src/tx/bot.go | 211 ++++++++++++++++++++++++++++++---------------- src/tx/command.go | 43 ++++++---- src/tx/context.go | 115 ++++++++++++++----------- src/tx/screen.go | 4 +- src/tx/session.go | 58 +++++++------ 8 files changed, 409 insertions(+), 204 deletions(-) diff --git a/cmd/test/main.go b/cmd/test/main.go index 40fdd01..1bb729c 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -150,13 +150,29 @@ func mutateMessage(fn func(string) string) tx.ActionFunc { } } +var gBeh = tx.NewGroupBehaviour(). + InitFunc(func(a *tx.GA) { + }). + WithCommands( + tx.NewGroupCommand("hello").ActionFunc(func(a *tx.GA) { + a.Send("Hello, World!") + }), + tx.NewGroupCommand("mycounter").ActionFunc(func(a *tx.GA) { + d := a.GetSessionValue().(*UserData) + a.Sendf("Your counter value is %d", d.Counter) + }), + ) + func main() { token := os.Getenv("BOT_TOKEN") - bot, err := tx.NewBot(token, beh, nil) + bot, err := tx.NewBot(token) if err != nil { log.Panic(err) } + bot = bot. + WithBehaviour(beh). + WithGroupBehaviour(gBeh) bot.Debug = true diff --git a/src/tx/action.go b/src/tx/action.go index 8e93d84..96756b8 100644 --- a/src/tx/action.go +++ b/src/tx/action.go @@ -1,10 +1,41 @@ package tx -import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) +//apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" -type Update = apix.Update +type Action interface { + Act(*Arg) +} + +type GroupAction interface { + Act(*GroupArg) +} + +// Customized actions for the bot. + +type ActionFunc func(*Arg) + +func (af ActionFunc) Act(a *Arg) { + af(a) +} + +type GroupActionFunc func(*GroupArg) + +func (af GroupActionFunc) Act(a *GroupArg) { + af(a) +} + +// The type implements changing screen to the underlying ScreenId +type ScreenChange ScreenId + +func (sc ScreenChange) Act(c *Arg) { + if !c.B.behaviour.ScreenExist(ScreenId(sc)) { + panic(ScreenNotExistErr) + } + err := c.ChangeScreen(ScreenId(sc)) + if err != nil { + panic(err) + } +} // The argument for handling. type Arg struct { @@ -15,14 +46,57 @@ type Arg struct { } type A = Arg +// Changes screen of user to the Id one. +func (c *Arg) ChangeScreen(screenId ScreenId) error { + // Return if it will not change anything. + if c.CurrentScreenId == screenId { + return nil + } + + if !c.B.behaviour.ScreenExist(screenId) { + return ScreenNotExistErr + } + + // Stop the reading by sending the nil. + if c.readingUpdate { + c.updates <- nil + } + + screen := c.B.behaviour.Screens[screenId] + screen.Render(c.Context) + + c.Session.ChangeScreen(screenId) + c.KeyboardId = screen.KeyboardId + + if screen.Action != nil { + c.run(screen.Action, c.U) + } + + return nil +} + +// The argument for handling in group behaviour. type GroupArg struct { - GroupArg *GroupContext - U *Update + *GroupContext + *Update } type GA = GroupArg -type Action interface { - Act(*Arg) +func (a *GA) SentFromSid() SessionId { + return SessionId(a.SentFrom().ID) +} + +func (a *GA) GetSessionValue() any { + v, _ := a.B.GetSessionValueBySid(a.SentFromSid()) + return v +} + +// The argument for handling in channenl behaviours. +type ChannelArg struct { +} +type CA = ChannelArg +type ChannelAction struct { + Act (*ChannelArg) } type JsonTyper interface { @@ -37,23 +111,3 @@ type JsonAction struct { func (ja JsonAction) UnmarshalJSON(bts []byte, ptr any) error { return nil } - -// Customized action for the bot. -type ActionFunc func(*Arg) - -// The type implements changing screen to the underlying ScreenId -type ScreenChange ScreenId - -func (sc ScreenChange) Act(c *Arg) { - if !c.B.ScreenExist(ScreenId(sc)) { - panic(ScreenNotExistErr) - } - err := c.ChangeScreen(ScreenId(sc)) - if err != nil { - panic(err) - } -} - -func (af ActionFunc) Act(c *Arg) { - af(c) -} diff --git a/src/tx/beh.go b/src/tx/beh.go index f96297f..39990f8 100644 --- a/src/tx/beh.go +++ b/src/tx/beh.go @@ -3,6 +3,10 @@ package tx // The package implements // behaviour for the Telegram bots. +// The type describes behaviour for the bot in channels. +type ChannelBehaviour struct { +} + // The type describes behaviour for the bot in personal chats. type Behaviour struct { Start Action @@ -11,18 +15,6 @@ type Behaviour struct { 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. func NewBehaviour() *Behaviour { return &Behaviour{ @@ -128,3 +120,41 @@ func (beh *Behaviour) GetScreen(id ScreenId) *Screen { screen := beh.Screens[id] return screen } + +// The type describes behaviour for the bot in group chats. +type GroupBehaviour struct { + Init GroupAction + // List of commands + Commands GroupCommandMap +} + +func NewGroupBehaviour() *GroupBehaviour { + return &GroupBehaviour{ + Commands: make(GroupCommandMap), + } +} + +func (b *GroupBehaviour) WithInitAction(a GroupAction) *GroupBehaviour { + b.Init = a + return b +} + +func (b *GroupBehaviour) InitFunc(fn GroupActionFunc) *GroupBehaviour { + return b.WithInitAction(fn) +} + +func (b *GroupBehaviour) WithCommands( + cmds ...*GroupCommand, +) *GroupBehaviour { + 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 +} diff --git a/src/tx/bot.go b/src/tx/bot.go index 8def42d..e58549e 100644 --- a/src/tx/bot.go +++ b/src/tx/bot.go @@ -1,60 +1,113 @@ package tx import ( - "fmt" + //"fmt" + + "errors" apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +type Update = apix.Update +type Chat = apix.Chat +type User = apix.User + // The wrapper around Telegram API. type Bot struct { *apix.BotAPI - *Behaviour - sessions SessionMap + Me *User + // Private bot behaviour. + behaviour *Behaviour + // Group bot behaviour. + groupBehaviour *GroupBehaviour + // Bot behaviour in channels. + channelBehaviour *ChannelBehaviour + sessions SessionMap + groupSessions GroupSessionMap } -// Return the new bot for running the Behaviour. -func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) { +// Return the new bot with empty sessions and behaviour. +func NewBot(token string) (*Bot, error) { bot, err := apix.NewBotAPI(token) if err != nil { return nil, err } - // Make new sessions if no current are provided. - if sessions == nil { - sessions = make(SessionMap) - } - return &Bot{ - BotAPI: bot, - Behaviour: beh, - sessions: make(SessionMap), + BotAPI: bot, }, nil } +func (bot *Bot) GetSessionValueBySid( + sid SessionId, +) (any, bool) { + v, ok := bot.sessions[sid] + return v.V, ok +} + +func (bot *Bot) GetGroupSessionValue( + sid SessionId, +) (any, bool) { + v, ok := bot.groupSessions[sid] + return v.V, ok +} + +func (b *Bot) WithBehaviour(beh *Behaviour) *Bot { + b.behaviour = beh + b.sessions = make(SessionMap) + return b +} + +func (b *Bot) WithSessions(sessions SessionMap) *Bot { + b.sessions = sessions + return b +} + +func (b *Bot) WithGroupBehaviour(beh *GroupBehaviour) *Bot { + b.groupBehaviour = beh + b.groupSessions = make(GroupSessionMap) + return b +} + +func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot { + b.groupSessions = sessions + return b +} + // Run the bot with the Behaviour. func (bot *Bot) Run() error { + if bot.behaviour == nil && + bot.groupBehaviour == nil { + return errors.New("no behaviour defined") + } bot.Debug = true uc := apix.NewUpdate(0) uc.Timeout = 60 updates := bot.GetUpdatesChan(uc) - privateChans := make(map[SessionId]chan *Update) - groupChans := make(map[SessionId]chan *Update) + handles := make(map[string]chan *Update) + + if bot.behaviour != nil { + chn := make(chan *Update) + handles["private"] = chn + go bot.handlePrivate(chn) + } + + if bot.groupBehaviour != nil { + chn := make(chan *Update) + handles["group"] = chn + handles["supergroup"] = chn + go bot.handleGroup(chn) + } + + me, _ := bot.GetMe() + bot.Me = &me for u := range updates { - var chatType string - - if u.Message != nil { - chatType = u.Message.Chat.Type - } else if u.CallbackQuery != nil { - chatType = u.Message.Chat.Type + chn, ok := handles[u.FromChat().Type] + if !ok { + continue } - switch chatType { - case "private": - bot.handlePrivate(&u, privateChans) - case "group", "supergroup": - bot.handleGroup(&u, groupChans) - } + chn <- &u } return nil @@ -62,55 +115,69 @@ func (bot *Bot) Run() error { // The function handles updates supposed for the private // chat with the bot. -func (bot *Bot) handlePrivate(u *Update, chans map[SessionId]chan *Update) { +func (bot *Bot) handlePrivate(updates chan *Update) { + chans := make(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) + for u := range updates { + 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() { + 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 } - } 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 { +} + +func (bot *Bot) handleGroup(updates chan *Update) { + var sid SessionId + chans := make(map[SessionId]chan *Update) + for u := range updates { + sid = SessionId(u.FromChat().ID) + // If no session add new. + if _, ok := bot.groupSessions[sid]; !ok { + bot.groupSessions.Add(sid) + session := bot.groupSessions[sid] + ctx := &GroupContext{ + B: bot, + GroupSession: session, + updates: make(chan *Update), + } + chn := make(chan *Update) + chans[sid] = chn + go ctx.handleUpdateChan(chn) + } + + chn := chans[sid] chn <- u } } - -// Not implemented yet. -func (bot *Bot) handleGroup(u *Update, chans map[SessionId]chan *Update) { -} diff --git a/src/tx/command.go b/src/tx/command.go index 58247c7..281280a 100644 --- a/src/tx/command.go +++ b/src/tx/command.go @@ -7,28 +7,14 @@ import ( ) 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 } +type CommandMap map[CommandName]*Command func NewCommand(name CommandName) *Command { return &Command{ @@ -49,3 +35,30 @@ func (c *Command) Desc(desc string) *Command { c.Description = desc return c } + +type GroupCommand struct { + Name CommandName + Description string + Action GroupAction +} +type GroupCommandMap map[CommandName]*GroupCommand + +func NewGroupCommand(name CommandName) *GroupCommand { + return &GroupCommand{ + Name: name, + } +} + +func (cmd *GroupCommand) WithAction(a GroupAction) *GroupCommand { + cmd.Action = a + return cmd +} + +func (cmd *GroupCommand) ActionFunc(fn GroupActionFunc) *GroupCommand { + return cmd.WithAction(fn) +} + +func (cmd *GroupCommand) Desc(desc string) *GroupCommand { + cmd.Description = desc + return cmd +} diff --git a/src/tx/context.go b/src/tx/context.go index 9c607c4..38e27ca 100644 --- a/src/tx/context.go +++ b/src/tx/context.go @@ -17,31 +17,26 @@ type Context struct { readingUpdate bool } -// Context for interaction inside groups. -type GroupContext struct { - *GroupSession - B *Bot -} - // Goroutie function to handle each user. func (c *Context) handleUpdateChan(updates chan *Update) { + var act Action bot := c.B session := c.Session - c.run(bot.Start, nil) + beh := bot.behaviour + c.run(beh.Start, nil) for u := range updates { - screen := bot.Screens[session.CurrentScreenId] + screen := bot.behaviour.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] + cmd, ok := beh.Commands[cmdName] if ok { act = cmd.Action } else { } } else { - kbd := bot.Keyboards[screen.KeyboardId] + kbd := beh.Keyboards[screen.KeyboardId] btns := kbd.buttonMap() text := u.Message.Text btn, ok := btns[text] @@ -65,10 +60,6 @@ func (c *Context) handleUpdateChan(updates chan *Update) { act = btn.Action } } - - if act != nil { - c.run(act, u) - } } else if u.CallbackQuery != nil { cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data) data := u.CallbackQuery.Data @@ -77,49 +68,26 @@ func (c *Context) handleUpdateChan(updates chan *Update) { if err != nil { panic(err) } - kbd := bot.Keyboards[screen.InlineKeyboardId] + kbd := beh.Keyboards[screen.InlineKeyboardId] btns := kbd.buttonMap() btn, ok := btns[data] if !ok && c.readingUpdate { c.updates <- u continue } - c.run(btn.Action, u) + act = btn.Action + } + if act != nil { + c.run(act, u) } } } func (c *Context) run(a Action, u *Update) { - go a.Act(&A{c, u}) -} - -// Changes screen of user to the Id one. -func (c *Arg) ChangeScreen(screenId ScreenId) error { - // Return if it will not change anything. - if c.CurrentScreenId == screenId { - return nil - } - - if !c.B.ScreenExist(screenId) { - return ScreenNotExistErr - } - - // Stop the reading by sending the nil. - if c.readingUpdate { - c.updates <- nil - } - - screen := c.B.Screens[screenId] - screen.Render(c.Context) - - c.Session.ChangeScreen(screenId) - c.KeyboardId = screen.KeyboardId - - if screen.Action != nil { - c.run(screen.Action, c.U) - } - - return nil + go a.Act(&A{ + Context: c, + U: u, + }) } // Returns the next update ignoring current screen. @@ -158,3 +126,56 @@ func (c *Context) Send(v ...any) error { func (c *Context) Sendf(format string, v ...any) error { return c.Send(fmt.Sprintf(format, v...)) } + +// Context for interaction inside groups. +type GroupContext struct { + *GroupSession + B *Bot + updates chan *Update +} + +func (c *GroupContext) run(a GroupAction, u *Update) { + go a.Act(&GA{ + GroupContext: c, + Update: u, + }) +} + +func (c *GroupContext) handleUpdateChan(updates chan *Update) { + var act GroupAction + beh := c.B.groupBehaviour + for u := range updates { + if u.Message != nil { + msg := u.Message + if msg.IsCommand() { + cmdName := CommandName(msg.Command()) + + // Skipping the commands sent not to us. + atName := msg.CommandWithAt()[len(cmdName)+1:] + if c.B.Me.UserName != atName { + continue + } + cmd, ok := beh.Commands[cmdName] + if !ok { + // Some lack of command handling + continue + } + act = cmd.Action + } + } + if act != nil { + c.run(act, u) + } + } +} + +func (c *GroupContext) Sendf(format string, v ...any) error { + return c.Send(fmt.Sprintf(format, v...)) +} + +// Sends into the chat specified values converted to strings. +func (c *GroupContext) Send(v ...any) error { + msg := apix.NewMessage(c.Id.ToTelegram(), fmt.Sprint(v...)) + _, err := c.B.Send(msg) + return err +} diff --git a/src/tx/screen.go b/src/tx/screen.go index 9ee0278..0de136b 100644 --- a/src/tx/screen.go +++ b/src/tx/screen.go @@ -76,7 +76,7 @@ func (s *Screen) Render(c *Context) error { msg := apix.NewMessage(id, s.Text.String()) if s.InlineKeyboardId != "" { - kbd, ok := c.B.Keyboards[s.InlineKeyboardId] + kbd, ok := c.B.behaviour.Keyboards[s.InlineKeyboardId] if !ok { return KeyboardNotExistErr } @@ -97,7 +97,7 @@ func (s *Screen) Render(c *Context) error { // Replace keyboard with the new one. if s.KeyboardId != "" { - kbd, ok := c.B.Keyboards[s.KeyboardId] + kbd, ok := c.B.behaviour.Keyboards[s.KeyboardId] if !ok { return KeyboardNotExistErr } diff --git a/src/tx/session.go b/src/tx/session.go index f4f9a19..66e8d5b 100644 --- a/src/tx/session.go +++ b/src/tx/session.go @@ -4,6 +4,11 @@ package tx // In fact is simply ID of the chat. type SessionId int64 +// Convert the SessionId to Telegram API's type. +func (si SessionId) ToTelegram() int64 { + return int64(si) +} + // The type represents current state of // user interaction per each of them. type Session struct { @@ -17,20 +22,6 @@ type Session struct { V any } -// The type represents map of sessions using -// as key. -type SessionMap map[SessionId]*Session - -// 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{ @@ -39,6 +30,28 @@ func NewSession(id SessionId) *Session { } } +// Changes screen of user to the Id one for the session. +func (c *Session) ChangeScreen(screenId ScreenId) { + c.PreviousScreenId = c.CurrentScreenId + c.CurrentScreenId = screenId +} + +// The type represents map of sessions using +// as key. +type SessionMap map[SessionId]*Session + +// Add new empty session by it's ID. +func (sm SessionMap) Add(sid SessionId) { + sm[sid] = NewSession(sid) +} + +// Session information for a group. +type GroupSession struct { + Id SessionId + // Information for each user in the group. + V map[SessionId]any +} + // Returns new empty group session with specified group and user IDs. func NewGroupSession(id SessionId) *GroupSession { return &GroupSession{ @@ -47,18 +60,9 @@ func NewGroupSession(id SessionId) *GroupSession { } } -// Changes screen of user to the Id one for the session. -func (c *Session) ChangeScreen(screenId ScreenId) { - c.PreviousScreenId = c.CurrentScreenId - c.CurrentScreenId = screenId -} +// Map for every group the bot is in. +type GroupSessionMap map[SessionId]*GroupSession -// Convert the SessionId to Telegram API's type. -func (si SessionId) ToTelegram() int64 { - return int64(si) -} - -// Add new empty session by it's ID. -func (sm SessionMap) Add(sid SessionId) { - sm[sid] = NewSession(sid) +func (sm GroupSessionMap) Add(sid SessionId) { + sm[sid] = NewGroupSession(sid) }