Got rid off map of keyboards.

This commit is contained in:
Andrey Parhomenko 2023-08-15 16:02:14 +03:00
parent c2562cc54c
commit 772adb7b8b
9 changed files with 228 additions and 183 deletions

View file

@ -12,20 +12,11 @@ type UserData struct {
Counter int
}
var startScreenButton = tx.NewButton("🏠 To the start screen").
ScreenChange("start")
var (
startScreenButton = tx.NewButton("🏠 To the start screen").
ScreenChange("start")
var beh = tx.NewBehaviour().
// The function will be called every time
// the bot is started.
OnStartFunc(func(c *tx.A) {
c.V = &UserData{}
c.ChangeScreen("start")
}).WithKeyboards(
// Increment/decrement keyboard.
tx.NewKeyboard("inc/dec").Row(
incDecKeyboard = tx.NewKeyboard("").Row(
tx.NewButton("+").ActionFunc(func(c *tx.A) {
d := c.V.(*UserData)
d.Counter++
@ -38,49 +29,60 @@ var beh = tx.NewBehaviour().
}),
).Row(
startScreenButton,
),
)
// The navigational keyboard.
tx.NewKeyboard("nav").Row(
tx.NewButton("Inc/Dec").ScreenChange("inc/dec"),
).Row(
navKeyboard = tx.NewKeyboard("Choose your interest").
WithOneTime(true).
Row(
tx.NewButton("Inc/Dec").ScreenChange("inc/dec"),
).Row(
tx.NewButton("Upper case").ScreenChange("upper-case"),
tx.NewButton("Lower case").ScreenChange("lower-case"),
).Row(
tx.NewButton("Send location").
WithSendLocation(true).
ActionFunc(func(c *tx.A) {
var err error
if c.U.Message.Location != nil {
l := c.U.Message.Location
err = c.Sendf(
"Longitude: %f\n"+
"Latitude: %f\n"+
"Heading: %d"+
"",
l.Longitude,
l.Latitude,
l.Heading,
)
} else {
err = c.Send("Somehow wrong location was sent")
}
if err != nil {
c.Send(err)
}
}),
),
tx.NewButton("Send location").ScreenChange("send-location"),
)
tx.NewKeyboard("istart").Row(
tx.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"),
),
sendLocationKeyboard = tx.NewKeyboard("Press the button to send your location").
Row(
tx.NewButton("Send location").
WithSendLocation(true).
ActionFunc(func(c *tx.A) {
var err error
if c.U.Message.Location != nil {
l := c.U.Message.Location
err = c.Sendf(
"Longitude: %f\n"+
"Latitude: %f\n"+
"Heading: %d"+
"",
l.Longitude,
l.Latitude,
l.Heading,
)
} else {
err = c.Send("Somehow wrong location was sent")
}
if err != nil {
c.Send(err)
}
}),
).Row(
startScreenButton,
)
// The keyboard to return to the start screen.
tx.NewKeyboard("nav-start").Row(
navToStartKeyboard = tx.NewKeyboard("").Row(
startScreenButton,
),
).WithScreens(
)
)
var beh = tx.NewBehaviour().
OnStartFunc(func(c *tx.A) {
// The function will be called every time
// the bot is started.
c.V = &UserData{}
c.ChangeScreen("start")
}).WithScreens(
tx.NewScreen("start").
WithText(
"The bot started!"+
@ -88,8 +90,14 @@ var beh = tx.NewBehaviour().
" understand of how the API works, so just"+
" horse around a bit to guess everything out"+
" by yourself!",
).Keyboard("nav").
IKeyboard("istart"),
).WithKeyboard(navKeyboard).
// The inline keyboard with link to GitHub page.
WithIKeyboard(
tx.NewKeyboard("istart").Row(
tx.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"),
),
),
tx.NewScreen("inc/dec").
WithText(
@ -98,7 +106,7 @@ var beh = tx.NewBehaviour().
"by saving the counter for each of users "+
"separately. ",
).
Keyboard("inc/dec").
WithKeyboard(incDecKeyboard).
// The function will be called when reaching the screen.
ActionFunc(func(c *tx.A) {
d := c.V.(*UserData)
@ -107,13 +115,27 @@ var beh = tx.NewBehaviour().
tx.NewScreen("upper-case").
WithText("Type text and the bot will send you the upper case version to you").
Keyboard("nav-start").
WithKeyboard(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToUpper)),
tx.NewScreen("lower-case").
WithText("Type text and the bot will send you the lower case version").
Keyboard("nav-start").
WithKeyboard(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToLower)),
tx.NewScreen("send-location").
WithText("Send your location and I will tell where you are!").
WithKeyboard(sendLocationKeyboard).
WithIKeyboard(
tx.NewKeyboard("").Row(
tx.NewButton("Check").
WithData("check").
ActionFunc(func(a *tx.A) {
d := a.V.(*UserData)
a.Sendf("Counter = %d", d.Counter)
}),
),
),
).WithCommands(
tx.NewCommand("hello").
Desc("sends the 'Hello, World!' message back").

View file

@ -48,26 +48,23 @@ 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.
// Stop the reading by sending the nil,
// since we change the screen and
// current goroutine needs to be stopped.
if c.readingUpdate {
c.updates <- nil
}
// Getting the screen and changing to
// then executing its action.
screen := c.B.behaviour.Screens[screenId]
c.prevScreen = c.curScreen
c.curScreen = screen
screen.Render(c.Context)
c.Session.ChangeScreen(screenId)
c.KeyboardId = screen.KeyboardId
if screen.Action != nil {
c.run(screen.Action, c.U)
}

View file

@ -3,10 +3,6 @@ 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
@ -35,29 +31,6 @@ func (b *Behaviour) OnStartFunc(
return b.WithStart(fn)
}
func (b *Behaviour) OnStartChangeScreen(
id ScreenId,
) *Behaviour {
return b.WithStart(ScreenChange(id))
}
// The function sets keyboards.
func (b *Behaviour) WithKeyboards(
kbds ...*Keyboard,
) *Behaviour {
for _, kbd := range kbds {
if kbd.Id == "" {
panic("empty keyboard ID")
}
_, ok := b.Keyboards[kbd.Id]
if ok {
panic("duplicate keyboard IDs")
}
b.Keyboards[kbd.Id] = kbd
}
return b
}
// The function sets screens.
func (b *Behaviour) WithScreens(
screens ...*Screen,
@ -90,21 +63,6 @@ func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour {
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]
@ -128,21 +86,27 @@ type GroupBehaviour struct {
Commands GroupCommandMap
}
// Returns new empty group behaviour object.
func NewGroupBehaviour() *GroupBehaviour {
return &GroupBehaviour{
Commands: make(GroupCommandMap),
}
}
// Sets an Action for initialization on each group connected to the
// group bot.
func (b *GroupBehaviour) WithInitAction(a GroupAction) *GroupBehaviour {
b.Init = a
return b
}
// The method reciveies a function to be called on initialization of the
// bot group bot.
func (b *GroupBehaviour) InitFunc(fn GroupActionFunc) *GroupBehaviour {
return b.WithInitAction(fn)
}
// The method sets group commands.
func (b *GroupBehaviour) WithCommands(
cmds ...*GroupCommand,
) *GroupBehaviour {
@ -158,3 +122,7 @@ func (b *GroupBehaviour) WithCommands(
}
return b
}
// The type describes behaviour for the bot in channels.
type ChannelBehaviour struct {
}

View file

@ -38,6 +38,11 @@ func (btn *Button) WithAction(a Action) *Button {
return btn
}
func (btn *Button) WithData(dat string) *Button {
btn.Data = dat
return btn
}
// Sets whether the button must send owner's location.
func (btn *Button) WithSendLocation(ok bool) *Button {
btn.SendLocation = ok

View file

@ -12,20 +12,20 @@ type Context struct {
*Session
B *Bot
updates chan *Update
// Is true if currently reading the Update.
readingUpdate bool
curScreen, prevScreen *Screen
}
// Goroutie function to handle each user.
func (c *Context) handleUpdateChan(updates chan *Update) {
var act Action
bot := c.B
session := c.Session
beh := bot.behaviour
c.run(beh.Start, nil)
for u := range updates {
screen := bot.behaviour.Screens[session.CurrentScreenId]
screen := c.curScreen
// The part is added to implement custom update handling.
if u.Message != nil {
if u.Message.IsCommand() && !c.readingUpdate {
@ -36,7 +36,13 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
} else {
}
} else {
kbd := beh.Keyboards[screen.KeyboardId]
kbd := screen.Keyboard
if kbd == nil {
if c.readingUpdate {
c.updates <- u
}
continue
}
btns := kbd.buttonMap()
text := u.Message.Text
btn, ok := btns[text]
@ -68,13 +74,24 @@ func (c *Context) handleUpdateChan(updates chan *Update) {
if err != nil {
panic(err)
}
kbd := beh.Keyboards[screen.InlineKeyboardId]
kbd := screen.InlineKeyboard
if kbd == nil {
if c.readingUpdate {
c.updates <- u
}
continue
}
btns := kbd.buttonMap()
btn, ok := btns[data]
if !ok && c.readingUpdate {
c.updates <- u
continue
}
if !ok {
c.Sendf("%q", btns)
continue
}
act = btn.Action
}
if act != nil {

View file

@ -10,10 +10,11 @@ type WrongUpdateType struct {
}
var (
ScreenNotExistErr = errors.New("screen does not exist")
SessionNotExistErr = errors.New("session does not exist")
KeyboardNotExistErr = errors.New("keyboard does not exist")
NotAvailableErr = errors.New("the context is not available")
ScreenNotExistErr = errors.New("screen does not exist")
SessionNotExistErr = errors.New("session does not exist")
KeyboardNotExistErr = errors.New("keyboard does not exist")
NotAvailableErr = errors.New("the context is not available")
EmptyKeyboardTextErr = errors.New("got empty text for a keyboard")
)
func (wut WrongUpdateType) Error() string {

View file

@ -23,19 +23,33 @@ type KeyboardId string
// The type represents reply keyboard which
// is supposed to be showed on a Screen.
type Keyboard struct {
Id KeyboardId
// Text to be displayed with the keyboard.
Text string
// Rows to be displayed once the
// keyboard is sent.
Rows []ButtonRow
OneTime bool
Inline bool
}
type KeyboardMap map[KeyboardId]*Keyboard
// Return the new reply keyboard with rows as specified.
func NewKeyboard(id KeyboardId) *Keyboard {
func NewKeyboard(text string) *Keyboard {
return &Keyboard{
Id: id,
Text: text,
}
}
func (kbd *Keyboard) TelegramMarkup() any {
if kbd.Inline {
return kbd.toTelegramInline()
}
return kbd.toTelegramReply()
}
// Adds a new button row to the current keyboard.
func (kbd *Keyboard) Row(btns ...*Button) *Keyboard {
// For empty row. We do not need that.
@ -46,8 +60,8 @@ func (kbd *Keyboard) Row(btns ...*Button) *Keyboard {
return kbd
}
// Convert the Keyboard to the Telegram API type.
func (kbd *Keyboard) ToTelegram() apix.ReplyKeyboardMarkup {
// Convert the Keyboard to the Telegram API type of reply keyboard.
func (kbd *Keyboard) toTelegramReply() apix.ReplyKeyboardMarkup {
rows := [][]apix.KeyboardButton{}
for _, row := range kbd.Rows {
buttons := []apix.KeyboardButton{}
@ -57,10 +71,14 @@ func (kbd *Keyboard) ToTelegram() apix.ReplyKeyboardMarkup {
rows = append(rows, buttons)
}
if kbd.OneTime {
return apix.NewOneTimeReplyKeyboard(rows...)
}
return apix.NewReplyKeyboard(rows...)
}
func (kbd *Keyboard) ToTelegramInline() apix.InlineKeyboardMarkup {
func (kbd *Keyboard) toTelegramInline() apix.InlineKeyboardMarkup {
rows := [][]apix.InlineKeyboardButton{}
for _, row := range kbd.Rows {
buttons := []apix.InlineKeyboardButton{}
@ -73,6 +91,16 @@ func (kbd *Keyboard) ToTelegramInline() apix.InlineKeyboardMarkup {
return apix.NewInlineKeyboardMarkup(rows...)
}
func (kbd *Keyboard) WithOneTime(oneTime bool) *Keyboard {
kbd.OneTime = oneTime
return kbd
}
func (kbd *Keyboard) WithInline(inline bool) *Keyboard {
kbd.Inline = inline
return kbd
}
// Returns the map of buttons. Used to define the Action.
func (kbd *Keyboard) buttonMap() ButtonMap {
ret := make(ButtonMap)

View file

@ -7,24 +7,17 @@ import (
// Unique identifier for the screen.
type ScreenId string
// Should be replaced with something that can be
// dinamicaly rendered. (WIP)
type ScreenText string
// Screen statement of the bot.
// Mostly what buttons to show.
type Screen struct {
Id ScreenId
// Text to be sent to the user when changing to the screen.
Text ScreenText
// The text to be displayed when the screen is
// reached.
Text string
// The keyboard to be sent in the message part.
InlineKeyboardId KeyboardId
InlineKeyboard *Keyboard
// Keyboard to be displayed on the screen.
KeyboardId KeyboardId
Keyboard *Keyboard
// Action called on the reaching the screen.
Action Action
}
@ -40,18 +33,22 @@ func NewScreen(id ScreenId) *Screen {
}
// Returns the screen with specified text printing on appearing.
func (s *Screen) WithText(text ScreenText) *Screen {
func (s *Screen) WithText(text string) *Screen {
s.Text = text
return s
}
func (s *Screen) IKeyboard(kbdId KeyboardId) *Screen {
s.InlineKeyboardId = kbdId
func (s *Screen) WithInlineKeyboard(ikbd *Keyboard) *Screen {
s.InlineKeyboard = ikbd
return s
}
func (s *Screen) Keyboard(kbdId KeyboardId) *Screen {
s.KeyboardId = kbdId
func (s *Screen) WithIKeyboard(ikbd *Keyboard) *Screen {
return s.WithInlineKeyboard(ikbd)
}
func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen {
s.Keyboard = kbd
return s
}
@ -64,49 +61,69 @@ func (s *Screen) ActionFunc(a ActionFunc) *Screen {
return s.WithAction(a)
}
// Rendering the screen text to string to be sent or printed.
func (st ScreenText) String() string {
return string(st)
}
// Renders output of the screen only to the side of the user.
func (s *Screen) Render(c *Context) error {
id := c.Id.ToTelegram()
kbd := s.Keyboard
iKbd := s.InlineKeyboard
msg := apix.NewMessage(id, s.Text.String())
var ch [2]apix.Chattable
var txt string
if s.InlineKeyboardId != "" {
kbd, ok := c.B.behaviour.Keyboards[s.InlineKeyboardId]
if !ok {
return KeyboardNotExistErr
// Screen text and inline keyboard.
if s.Text != "" {
txt = s.Text
} else if iKbd != nil {
if iKbd.Text != "" {
txt = iKbd.Text
} else {
// Default to send the keyboard.
txt = ">"
}
msg.ReplyMarkup = kbd.ToTelegramInline()
}
_, err := c.B.Send(msg)
if err != nil {
return err
}
msg = apix.NewMessage(id, ">")
// Checking if we need to resend the keyboard.
if s.KeyboardId != c.KeyboardId {
// Remove keyboard by default.
var tkbd any
tkbd = apix.NewRemoveKeyboard(true)
// Replace keyboard with the new one.
if s.KeyboardId != "" {
kbd, ok := c.B.behaviour.Keyboards[s.KeyboardId]
if !ok {
return KeyboardNotExistErr
if txt != "" {
msg := apix.NewMessage(id, txt)
if iKbd != nil {
msg.ReplyMarkup = iKbd.toTelegramInline()
} else if kbd != nil {
msg.ReplyMarkup = kbd.toTelegramReply()
if _, err := c.B.Send(msg); err != nil {
return err
}
tkbd = kbd.ToTelegram()
return nil
} else {
msg.ReplyMarkup = apix.NewRemoveKeyboard(true)
if _, err := c.B.Send(msg); err != nil {
return err
}
return nil
}
ch[0] = msg
}
msg.ReplyMarkup = tkbd
if _, err := c.B.Send(msg); err != nil {
return err
// Screen text and reply keyboard.
txt = ""
if kbd != nil {
if kbd.Text != "" {
txt = kbd.Text
} else {
txt = ">"
}
msg := apix.NewMessage(id, txt)
msg.ReplyMarkup = kbd.toTelegramReply()
ch[1] = msg
} else {
// Removing keyboard if there is none.
msg := apix.NewMessage(id, ">")
msg.ReplyMarkup = apix.NewRemoveKeyboard(true)
ch[1] = msg
}
for _, m := range ch {
if m != nil {
if _, err := c.B.Send(m); err != nil {
return err
}
}
}

View file

@ -12,14 +12,10 @@ func (si SessionId) ToTelegram() int64 {
// The type represents current state of
// user interaction per each of them.
type Session struct {
// Id of the chat of the user.
Id SessionId
// Current screen identifier.
CurrentScreenId ScreenId
// ID of the previous screen.
PreviousScreenId ScreenId
// The currently showed on display keyboard inside Action.
KeyboardId KeyboardId
V any
// Custom value for each user.
V any
}
// Return new empty session with specified user ID.
@ -30,12 +26,6 @@ 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