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() {
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

View file

@ -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)
}

View file

@ -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
}

View file

@ -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) {
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}