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

View file

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

View file

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

View file

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

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

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

View file

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