Implemented basic command handling.

This commit is contained in:
Andrey Parhomenko 2023-08-12 14:35:33 +03:00
parent d3922a14e1
commit 44185e92af
7 changed files with 258 additions and 105 deletions

View file

@ -8,6 +8,10 @@ import (
"github.com/mojosa-software/got/src/tx" "github.com/mojosa-software/got/src/tx"
) )
type UserData struct {
Counter int
}
var startScreenButton = tx.NewButton(). var startScreenButton = tx.NewButton().
WithText("🏠 To the start screen"). WithText("🏠 To the start screen").
ScreenChange("start") ScreenChange("start")
@ -17,21 +21,21 @@ var beh = tx.NewBehaviour().
// The function will be called every time // The function will be called every time
// the bot is started. // the bot is started.
OnStartFunc(func(c *tx.Context) { OnStartFunc(func(c *tx.Context) {
c.V["counter"] = new(int) c.V = &UserData{}
c.ChangeScreen("start") c.ChangeScreen("start")
}).WithKeyboards( }).WithKeyboards(
// Increment/decrement keyboard. // Increment/decrement keyboard.
tx.NewKeyboard("inc/dec").Row( tx.NewKeyboard("inc/dec").Row(
tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) { tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int) d := c.V.(*UserData)
*counter++ d.Counter++
c.Sendf("%d", *counter) c.Sendf("%d", d.Counter)
}), }),
tx.NewButton().WithText("-").ActionFunc(func(c *tx.Context) { tx.NewButton().WithText("-").ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int) d := c.V.(*UserData)
*counter-- d.Counter--
c.Sendf("%d", *counter) c.Sendf("%d", d.Counter)
}), }),
).Row( ).Row(
startScreenButton, startScreenButton,
@ -75,8 +79,8 @@ var beh = tx.NewBehaviour().
Keyboard("inc/dec"). Keyboard("inc/dec").
// The function will be called when reaching the screen. // The function will be called when reaching the screen.
ActionFunc(func(c *tx.Context) { ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int) d := c.V.(*UserData)
c.Sendf("Current counter value = %d", *counter) c.Sendf("Current counter value = %d", d.Counter)
}), }),
tx.NewScreen("upper-case"). 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"). WithText("Type text and the bot will send you the lower case version").
Keyboard("nav-start"). Keyboard("nav-start").
ActionFunc(mutateMessage(strings.ToLower)), 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 { func mutateMessage(fn func(string) string) tx.ActionFunc {

View file

@ -3,11 +3,24 @@ 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. // The type describes behaviour for the bot in personal chats.
type Behaviour struct { type Behaviour struct {
Start Action Start Action
Screens ScreenMap Screens ScreenMap
Keyboards KeyboardMap 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. // Returns new empty behaviour.
@ -15,6 +28,7 @@ func NewBehaviour() *Behaviour {
return &Behaviour{ return &Behaviour{
Screens: make(ScreenMap), Screens: make(ScreenMap),
Keyboards: make(KeyboardMap), Keyboards: make(KeyboardMap),
Commands: make(CommandMap),
} }
} }
@ -69,6 +83,36 @@ func (b *Behaviour) WithScreens(
return b 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. // Check whether the screen exists in the behaviour.
func (beh *Behaviour) ScreenExist(id ScreenId) bool { func (beh *Behaviour) ScreenExist(id ScreenId) bool {
_, ok := beh.Screens[id] _, ok := beh.Screens[id]

View file

@ -1,8 +1,9 @@
package tx package tx
import ( import (
"fmt"
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" apix "github.com/go-telegram-bot-api/telegram-bot-api/v5"
//"log"
) )
// The wrapper around Telegram API. // The wrapper around Telegram API.
@ -34,16 +35,45 @@ func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) {
// Run the bot with the Behaviour. // Run the bot with the Behaviour.
func (bot *Bot) Run() error { func (bot *Bot) Run() error {
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)
chans := make(map[SessionId]chan *Update) groupChans := make(map[SessionId]chan *Update)
for u := range updates { 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
}
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 var sid SessionId
if u.Message != nil { 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 // Create new session if the one does not exist
// for this user. // for this user.
sid = SessionId(u.Message.Chat.ID) sid = SessionId(u.Message.Chat.ID)
@ -54,8 +84,8 @@ func (bot *Bot) Run() error {
// The "start" command resets the bot // The "start" command resets the bot
// by executing the Start Action. // by executing the Start Action.
if u.Message.IsCommand() { if u.Message.IsCommand() {
cmd := u.Message.Command() cmdName := CommandName(u.Message.Command())
if cmd == "start" { if cmdName == "start" {
// Getting current session and context. // Getting current session and context.
session := bot.sessions[sid] session := bot.sessions[sid]
ctx := &Context{ ctx := &Context{
@ -68,17 +98,19 @@ func (bot *Bot) Run() error {
chans[sid] = chn chans[sid] = chn
// Starting the goroutine for the user. // Starting the goroutine for the user.
go ctx.handleUpdateChan(chn) go ctx.handleUpdateChan(chn)
continue
} }
} }
} else if u.CallbackQuery != nil { } else if u.CallbackQuery != nil {
sid = SessionId(u.CallbackQuery.Message.Chat.ID) sid = SessionId(u.CallbackQuery.Message.Chat.ID)
} }
chn, ok := chans[sid] chn, ok := chans[sid]
// The bot MUST get the "start" command.
// It will do nothing otherwise.
if ok { if ok {
chn <- &u chn <- u
} }
} }
return nil // Not implemented yet.
func (bot *Bot) handleGroup(u *Update, chans map[SessionId]chan *Update) {
} }

View file

@ -45,22 +45,6 @@ func (btn *Button) ScreenChange(sc ScreenChange) *Button {
return btn.WithAction(sc) 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 { func (btn *Button) ToTelegram() apix.KeyboardButton {
return apix.NewKeyboardButton(btn.Text) return apix.NewKeyboardButton(btn.Text)
} }

51
src/tx/command.go Normal file
View file

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

View file

@ -12,8 +12,15 @@ type Context struct {
*Session *Session
B *Bot B *Bot
updates chan *Update updates chan *Update
available bool
availableChan chan bool // 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. // Goroutie function to handle each user.
@ -25,19 +32,19 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
screen := bot.Screens[session.CurrentScreenId] screen := bot.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 {
cmdName := CommandName(u.Message.Command())
cmd, ok := bot.Behaviour.Commands[cmdName]
if ok {
act = cmd.Action
} else {
}
} else {
kbd := bot.Keyboards[screen.KeyboardId] kbd := bot.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]
/*if ok {
c.available = false
btn.Action.Act(c)
c.available = true
continue
}*/
// Sending wrong messages to // Sending wrong messages to
// the currently reading goroutine. // the currently reading goroutine.
if !ok && c.readingUpdate { if !ok && c.readingUpdate {
@ -45,8 +52,13 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
continue continue
} }
if ok && btn.Action != nil { if ok {
c.run(btn.Action) act = btn.Action
}
}
if act != nil {
c.run(act)
} }
} 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)
@ -68,10 +80,6 @@ func (c *Context) run(a Action) {
go a.Act(c) go a.Act(c)
} }
func (c *Context) Available() bool {
return c.available
}
// Changes screen of user to the Id one. // Changes screen of user to the Id one.
func (c *Context) ChangeScreen(screenId ScreenId) error { func (c *Context) ChangeScreen(screenId ScreenId) error {
// Return if it will not change anything. // Return if it will not change anything.
@ -83,15 +91,17 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
return ScreenNotExistErr return ScreenNotExistErr
} }
// Stop the reading by sending the nil.
if c.readingUpdate {
c.updates <- nil
}
screen := c.B.Screens[screenId] screen := c.B.Screens[screenId]
screen.Render(c) screen.Render(c)
c.Session.ChangeScreen(screenId) c.Session.ChangeScreen(screenId)
c.KeyboardId = screen.KeyboardId c.KeyboardId = screen.KeyboardId
if c.readingUpdate {
c.updates <- nil
}
if screen.Action != nil { if screen.Action != nil {
c.run(screen.Action) c.run(screen.Action)
} }

View file

@ -13,7 +13,6 @@ type SessionId int64
// 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 {
// Unique identifier for the session, Telegram chat's ID.
Id SessionId Id SessionId
// Current screen identifier. // Current screen identifier.
CurrentScreenId ScreenId CurrentScreenId ScreenId
@ -21,19 +20,24 @@ type Session struct {
PreviousScreenId ScreenId PreviousScreenId ScreenId
// The currently showed on display keyboard inside Action. // The currently showed on display keyboard inside Action.
KeyboardId KeyboardId KeyboardId KeyboardId
V any
// Is true if currently reading the Update.
readingUpdate bool
// Custom data for each user.
V map[string]any
} }
// The type represents map of sessions using // The type represents map of sessions using
// as key. // as key.
type SessionMap map[SessionId]*Session 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 { func NewSession(id SessionId) *Session {
return &Session{ return &Session{
Id: id, 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. // Changes screen of user to the Id one for the session.
func (c *Session) ChangeScreen(screenId ScreenId) { func (c *Session) ChangeScreen(screenId ScreenId) {
c.PreviousScreenId = c.CurrentScreenId c.PreviousScreenId = c.CurrentScreenId