Implemented basic group behaviour.

This commit is contained in:
Andrey Parhomenko 2023-08-13 15:37:36 +03:00
parent e3045862c4
commit c2562cc54c
8 changed files with 409 additions and 204 deletions

View file

@ -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() { func main() {
token := os.Getenv("BOT_TOKEN") token := os.Getenv("BOT_TOKEN")
bot, err := tx.NewBot(token, beh, nil) bot, err := tx.NewBot(token)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
bot = bot.
WithBehaviour(beh).
WithGroupBehaviour(gBeh)
bot.Debug = true bot.Debug = true

View file

@ -1,10 +1,41 @@
package tx 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. // The argument for handling.
type Arg struct { type Arg struct {
@ -15,14 +46,57 @@ type Arg struct {
} }
type A = Arg 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 { type GroupArg struct {
GroupArg *GroupContext *GroupContext
U *Update *Update
} }
type GA = GroupArg type GA = GroupArg
type Action interface { func (a *GA) SentFromSid() SessionId {
Act(*Arg) 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 { type JsonTyper interface {
@ -37,23 +111,3 @@ type JsonAction struct {
func (ja JsonAction) UnmarshalJSON(bts []byte, ptr any) error { func (ja JsonAction) UnmarshalJSON(bts []byte, ptr any) error {
return nil 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)
}

View file

@ -3,6 +3,10 @@ package tx
// The package implements // The package implements
// behaviour for the Telegram bots. // 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. // The type describes behaviour for the bot in personal chats.
type Behaviour struct { type Behaviour struct {
Start Action Start Action
@ -11,18 +15,6 @@ type Behaviour struct {
Commands CommandMap 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. // Returns new empty behaviour.
func NewBehaviour() *Behaviour { func NewBehaviour() *Behaviour {
return &Behaviour{ return &Behaviour{
@ -128,3 +120,41 @@ func (beh *Behaviour) GetScreen(id ScreenId) *Screen {
screen := beh.Screens[id] screen := beh.Screens[id]
return screen 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
}

View file

@ -1,60 +1,113 @@
package tx package tx
import ( import (
"fmt" //"fmt"
"errors"
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 Chat = apix.Chat
type User = apix.User
// The wrapper around Telegram API. // The wrapper around Telegram API.
type Bot struct { type Bot struct {
*apix.BotAPI *apix.BotAPI
*Behaviour Me *User
sessions SessionMap // 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. // Return the new bot with empty sessions and behaviour.
func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) { func NewBot(token string) (*Bot, error) {
bot, err := apix.NewBotAPI(token) bot, err := apix.NewBotAPI(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Make new sessions if no current are provided.
if sessions == nil {
sessions = make(SessionMap)
}
return &Bot{ return &Bot{
BotAPI: bot, BotAPI: bot,
Behaviour: beh,
sessions: make(SessionMap),
}, nil }, 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. // Run the bot with the Behaviour.
func (bot *Bot) Run() error { func (bot *Bot) Run() error {
if bot.behaviour == nil &&
bot.groupBehaviour == nil {
return errors.New("no behaviour defined")
}
bot.Debug = true bot.Debug = true
uc := apix.NewUpdate(0) uc := apix.NewUpdate(0)
uc.Timeout = 60 uc.Timeout = 60
updates := bot.GetUpdatesChan(uc) updates := bot.GetUpdatesChan(uc)
privateChans := make(map[SessionId]chan *Update) handles := make(map[string]chan *Update)
groupChans := make(map[SessionId]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 { for u := range updates {
var chatType string chn, ok := handles[u.FromChat().Type]
if !ok {
if u.Message != nil { continue
chatType = u.Message.Chat.Type
} else if u.CallbackQuery != nil {
chatType = u.Message.Chat.Type
} }
switch chatType { chn <- &u
case "private":
bot.handlePrivate(&u, privateChans)
case "group", "supergroup":
bot.handleGroup(&u, groupChans)
}
} }
return nil return nil
@ -62,55 +115,69 @@ func (bot *Bot) Run() error {
// The function handles updates supposed for the private // The function handles updates supposed for the private
// chat with the bot. // 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 var sid SessionId
if u.Message != nil { for u := range updates {
msg := u.Message if u.Message != nil {
// Create new session if the one does not exist
if bot.Debug { // for this user.
fmt.Printf("is command: %q\n", u.Message.IsCommand()) sid = SessionId(u.Message.Chat.ID)
fmt.Printf("command itself: %q\n", msg.Command()) if _, ok := bot.sessions[sid]; !ok {
fmt.Printf("command arguments: %q\n", msg.CommandArguments()) bot.sessions.Add(sid)
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)
} }
// 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. func (bot *Bot) handleGroup(updates chan *Update) {
if ok { 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 chn <- u
} }
} }
// Not implemented yet.
func (bot *Bot) handleGroup(u *Update, chans map[SessionId]chan *Update) {
}

View file

@ -7,28 +7,14 @@ import (
) )
type Message = apix.Message type Message = apix.Message
type CommandName string 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 { type Command struct {
Name CommandName Name CommandName
Description string Description string
Action Action Action Action
} }
type CommandMap map[CommandName]*Command
func NewCommand(name CommandName) *Command { func NewCommand(name CommandName) *Command {
return &Command{ return &Command{
@ -49,3 +35,30 @@ func (c *Command) Desc(desc string) *Command {
c.Description = desc c.Description = desc
return c 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
}

View file

@ -17,31 +17,26 @@ type Context struct {
readingUpdate bool readingUpdate bool
} }
// Context for interaction inside groups.
type GroupContext struct {
*GroupSession
B *Bot
}
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *Context) handleUpdateChan(updates chan *Update) { func (c *Context) handleUpdateChan(updates chan *Update) {
var act Action
bot := c.B bot := c.B
session := c.Session session := c.Session
c.run(bot.Start, nil) beh := bot.behaviour
c.run(beh.Start, nil)
for u := range updates { for u := range updates {
screen := bot.Screens[session.CurrentScreenId] screen := bot.behaviour.Screens[session.CurrentScreenId]
// The part is added to implement custom update handling. // The part is added to implement custom update handling.
if u.Message != nil { if u.Message != nil {
var act Action
if u.Message.IsCommand() && !c.readingUpdate { if u.Message.IsCommand() && !c.readingUpdate {
cmdName := CommandName(u.Message.Command()) cmdName := CommandName(u.Message.Command())
cmd, ok := bot.Behaviour.Commands[cmdName] cmd, ok := beh.Commands[cmdName]
if ok { if ok {
act = cmd.Action act = cmd.Action
} else { } else {
} }
} else { } else {
kbd := bot.Keyboards[screen.KeyboardId] kbd := beh.Keyboards[screen.KeyboardId]
btns := kbd.buttonMap() btns := kbd.buttonMap()
text := u.Message.Text text := u.Message.Text
btn, ok := btns[text] btn, ok := btns[text]
@ -65,10 +60,6 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
act = btn.Action act = btn.Action
} }
} }
if act != nil {
c.run(act, u)
}
} else if u.CallbackQuery != nil { } else if u.CallbackQuery != nil {
cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data) cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data)
data := u.CallbackQuery.Data data := u.CallbackQuery.Data
@ -77,49 +68,26 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
kbd := bot.Keyboards[screen.InlineKeyboardId] kbd := beh.Keyboards[screen.InlineKeyboardId]
btns := kbd.buttonMap() btns := kbd.buttonMap()
btn, ok := btns[data] btn, ok := btns[data]
if !ok && c.readingUpdate { if !ok && c.readingUpdate {
c.updates <- u c.updates <- u
continue continue
} }
c.run(btn.Action, u) act = btn.Action
}
if act != nil {
c.run(act, u)
} }
} }
} }
func (c *Context) run(a Action, u *Update) { func (c *Context) run(a Action, u *Update) {
go a.Act(&A{c, u}) go a.Act(&A{
} Context: c,
U: 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
} }
// Returns the next update ignoring current screen. // 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 { func (c *Context) Sendf(format string, v ...any) error {
return c.Send(fmt.Sprintf(format, v...)) 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
}

View file

@ -76,7 +76,7 @@ func (s *Screen) Render(c *Context) error {
msg := apix.NewMessage(id, s.Text.String()) msg := apix.NewMessage(id, s.Text.String())
if s.InlineKeyboardId != "" { if s.InlineKeyboardId != "" {
kbd, ok := c.B.Keyboards[s.InlineKeyboardId] kbd, ok := c.B.behaviour.Keyboards[s.InlineKeyboardId]
if !ok { if !ok {
return KeyboardNotExistErr return KeyboardNotExistErr
} }
@ -97,7 +97,7 @@ func (s *Screen) Render(c *Context) error {
// Replace keyboard with the new one. // Replace keyboard with the new one.
if s.KeyboardId != "" { if s.KeyboardId != "" {
kbd, ok := c.B.Keyboards[s.KeyboardId] kbd, ok := c.B.behaviour.Keyboards[s.KeyboardId]
if !ok { if !ok {
return KeyboardNotExistErr return KeyboardNotExistErr
} }

View file

@ -4,6 +4,11 @@ package tx
// In fact is simply ID of the chat. // In fact is simply ID of the chat.
type SessionId int64 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 // The type represents current state of
// user interaction per each of them. // user interaction per each of them.
type Session struct { type Session struct {
@ -17,20 +22,6 @@ type Session struct {
V any 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. // Return new empty session with specified user ID.
func NewSession(id SessionId) *Session { func NewSession(id SessionId) *Session {
return &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. // Returns new empty group session with specified group and user IDs.
func NewGroupSession(id SessionId) *GroupSession { func NewGroupSession(id SessionId) *GroupSession {
return &GroupSession{ return &GroupSession{
@ -47,18 +60,9 @@ func NewGroupSession(id SessionId) *GroupSession {
} }
} }
// Changes screen of user to the Id one for the session. // Map for every group the bot is in.
func (c *Session) ChangeScreen(screenId ScreenId) { type GroupSessionMap map[SessionId]*GroupSession
c.PreviousScreenId = c.CurrentScreenId
c.CurrentScreenId = screenId
}
// Convert the SessionId to Telegram API's type. func (sm GroupSessionMap) Add(sid SessionId) {
func (si SessionId) ToTelegram() int64 { sm[sid] = NewGroupSession(sid)
return int64(si)
}
// Add new empty session by it's ID.
func (sm SessionMap) Add(sid SessionId) {
sm[sid] = NewSession(sid)
} }