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"
|
"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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue