Implemented basic command handling.
This commit is contained in:
parent
d3922a14e1
commit
44185e92af
7 changed files with 258 additions and 105 deletions
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
106
src/tx/bot.go
106
src/tx/bot.go
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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
51
src/tx/command.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue