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 Counter int
} }
var startScreenButton = tx.NewButton("🏠 To the start screen"). var (
ScreenChange("start") startScreenButton = tx.NewButton("🏠 To the start screen").
ScreenChange("start")
var beh = tx.NewBehaviour(). incDecKeyboard = tx.NewKeyboard("").Row(
// 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(
tx.NewButton("+").ActionFunc(func(c *tx.A) { tx.NewButton("+").ActionFunc(func(c *tx.A) {
d := c.V.(*UserData) d := c.V.(*UserData)
d.Counter++ d.Counter++
@ -38,49 +29,60 @@ var beh = tx.NewBehaviour().
}), }),
).Row( ).Row(
startScreenButton, startScreenButton,
), )
// The navigational keyboard. navKeyboard = tx.NewKeyboard("Choose your interest").
tx.NewKeyboard("nav").Row( WithOneTime(true).
tx.NewButton("Inc/Dec").ScreenChange("inc/dec"), Row(
).Row( tx.NewButton("Inc/Dec").ScreenChange("inc/dec"),
).Row(
tx.NewButton("Upper case").ScreenChange("upper-case"), tx.NewButton("Upper case").ScreenChange("upper-case"),
tx.NewButton("Lower case").ScreenChange("lower-case"), tx.NewButton("Lower case").ScreenChange("lower-case"),
).Row( ).Row(
tx.NewButton("Send location"). tx.NewButton("Send location").ScreenChange("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.NewKeyboard("istart").Row( sendLocationKeyboard = tx.NewKeyboard("Press the button to send your location").
tx.NewButton("GoT Github page"). Row(
WithUrl("https://github.com/mojosa-software/got"), 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. // The keyboard to return to the start screen.
tx.NewKeyboard("nav-start").Row( navToStartKeyboard = tx.NewKeyboard("").Row(
startScreenButton, 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"). tx.NewScreen("start").
WithText( WithText(
"The bot started!"+ "The bot started!"+
@ -88,8 +90,14 @@ var beh = tx.NewBehaviour().
" understand of how the API works, so just"+ " understand of how the API works, so just"+
" horse around a bit to guess everything out"+ " horse around a bit to guess everything out"+
" by yourself!", " by yourself!",
).Keyboard("nav"). ).WithKeyboard(navKeyboard).
IKeyboard("istart"), // 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"). tx.NewScreen("inc/dec").
WithText( WithText(
@ -98,7 +106,7 @@ var beh = tx.NewBehaviour().
"by saving the counter for each of users "+ "by saving the counter for each of users "+
"separately. ", "separately. ",
). ).
Keyboard("inc/dec"). WithKeyboard(incDecKeyboard).
// The function will be called when reaching the screen. // The function will be called when reaching the screen.
ActionFunc(func(c *tx.A) { ActionFunc(func(c *tx.A) {
d := c.V.(*UserData) d := c.V.(*UserData)
@ -107,13 +115,27 @@ var beh = tx.NewBehaviour().
tx.NewScreen("upper-case"). tx.NewScreen("upper-case").
WithText("Type text and the bot will send you the upper case version to you"). WithText("Type text and the bot will send you the upper case version to you").
Keyboard("nav-start"). WithKeyboard(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToUpper)), ActionFunc(mutateMessage(strings.ToUpper)),
tx.NewScreen("lower-case"). tx.NewScreen("lower-case").
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"). WithKeyboard(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToLower)), 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( ).WithCommands(
tx.NewCommand("hello"). tx.NewCommand("hello").
Desc("sends the 'Hello, World!' message back"). Desc("sends the 'Hello, World!' message back").

View file

@ -48,26 +48,23 @@ type A = Arg
// Changes screen of user to the Id one. // Changes screen of user to the Id one.
func (c *Arg) ChangeScreen(screenId ScreenId) error { 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) { if !c.B.behaviour.ScreenExist(screenId) {
return ScreenNotExistErr 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 { if c.readingUpdate {
c.updates <- nil c.updates <- nil
} }
// Getting the screen and changing to
// then executing its action.
screen := c.B.behaviour.Screens[screenId] screen := c.B.behaviour.Screens[screenId]
c.prevScreen = c.curScreen
c.curScreen = screen
screen.Render(c.Context) screen.Render(c.Context)
c.Session.ChangeScreen(screenId)
c.KeyboardId = screen.KeyboardId
if screen.Action != nil { if screen.Action != nil {
c.run(screen.Action, c.U) c.run(screen.Action, c.U)
} }

View file

@ -3,10 +3,6 @@ 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 in channels.
type ChannelBehaviour struct {
}
// The type describes behaviour for the bot in personal chats. // The type describes behaviour for the bot in personal chats.
type Behaviour struct { type Behaviour struct {
Start Action Start Action
@ -35,29 +31,6 @@ func (b *Behaviour) OnStartFunc(
return b.WithStart(fn) 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. // The function sets screens.
func (b *Behaviour) WithScreens( func (b *Behaviour) WithScreens(
screens ...*Screen, screens ...*Screen,
@ -90,21 +63,6 @@ func (b *Behaviour) WithCommands(cmds ...*Command) *Behaviour {
return b 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]
@ -128,21 +86,27 @@ type GroupBehaviour struct {
Commands GroupCommandMap Commands GroupCommandMap
} }
// Returns new empty group behaviour object.
func NewGroupBehaviour() *GroupBehaviour { func NewGroupBehaviour() *GroupBehaviour {
return &GroupBehaviour{ return &GroupBehaviour{
Commands: make(GroupCommandMap), Commands: make(GroupCommandMap),
} }
} }
// Sets an Action for initialization on each group connected to the
// group bot.
func (b *GroupBehaviour) WithInitAction(a GroupAction) *GroupBehaviour { func (b *GroupBehaviour) WithInitAction(a GroupAction) *GroupBehaviour {
b.Init = a b.Init = a
return b return b
} }
// The method reciveies a function to be called on initialization of the
// bot group bot.
func (b *GroupBehaviour) InitFunc(fn GroupActionFunc) *GroupBehaviour { func (b *GroupBehaviour) InitFunc(fn GroupActionFunc) *GroupBehaviour {
return b.WithInitAction(fn) return b.WithInitAction(fn)
} }
// The method sets group commands.
func (b *GroupBehaviour) WithCommands( func (b *GroupBehaviour) WithCommands(
cmds ...*GroupCommand, cmds ...*GroupCommand,
) *GroupBehaviour { ) *GroupBehaviour {
@ -158,3 +122,7 @@ func (b *GroupBehaviour) WithCommands(
} }
return b 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 return btn
} }
func (btn *Button) WithData(dat string) *Button {
btn.Data = dat
return btn
}
// Sets whether the button must send owner's location. // Sets whether the button must send owner's location.
func (btn *Button) WithSendLocation(ok bool) *Button { func (btn *Button) WithSendLocation(ok bool) *Button {
btn.SendLocation = ok btn.SendLocation = ok

View file

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

View file

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

View file

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

View file

@ -7,24 +7,17 @@ import (
// Unique identifier for the screen. // Unique identifier for the screen.
type ScreenId string type ScreenId string
// Should be replaced with something that can be
// dinamicaly rendered. (WIP)
type ScreenText string
// Screen statement of the bot. // Screen statement of the bot.
// Mostly what buttons to show. // Mostly what buttons to show.
type Screen struct { type Screen struct {
Id ScreenId Id ScreenId
// The text to be displayed when the screen is
// Text to be sent to the user when changing to the screen. // reached.
Text ScreenText Text string
// The keyboard to be sent in the message part. // The keyboard to be sent in the message part.
InlineKeyboardId KeyboardId InlineKeyboard *Keyboard
// Keyboard to be displayed on the screen. // Keyboard to be displayed on the screen.
KeyboardId KeyboardId Keyboard *Keyboard
// Action called on the reaching the screen. // Action called on the reaching the screen.
Action Action Action Action
} }
@ -40,18 +33,22 @@ func NewScreen(id ScreenId) *Screen {
} }
// Returns the screen with specified text printing on appearing. // 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 s.Text = text
return s return s
} }
func (s *Screen) IKeyboard(kbdId KeyboardId) *Screen { func (s *Screen) WithInlineKeyboard(ikbd *Keyboard) *Screen {
s.InlineKeyboardId = kbdId s.InlineKeyboard = ikbd
return s return s
} }
func (s *Screen) Keyboard(kbdId KeyboardId) *Screen { func (s *Screen) WithIKeyboard(ikbd *Keyboard) *Screen {
s.KeyboardId = kbdId return s.WithInlineKeyboard(ikbd)
}
func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen {
s.Keyboard = kbd
return s return s
} }
@ -64,49 +61,69 @@ func (s *Screen) ActionFunc(a ActionFunc) *Screen {
return s.WithAction(a) 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. // Renders output of the screen only to the side of the user.
func (s *Screen) Render(c *Context) error { func (s *Screen) Render(c *Context) error {
id := c.Id.ToTelegram() 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 != "" { // Screen text and inline keyboard.
kbd, ok := c.B.behaviour.Keyboards[s.InlineKeyboardId] if s.Text != "" {
if !ok { txt = s.Text
return KeyboardNotExistErr } else if iKbd != nil {
if iKbd.Text != "" {
txt = iKbd.Text
} else {
// Default to send the keyboard.
txt = ">"
} }
msg.ReplyMarkup = kbd.ToTelegramInline()
} }
if txt != "" {
_, err := c.B.Send(msg) msg := apix.NewMessage(id, txt)
if err != nil { if iKbd != nil {
return err msg.ReplyMarkup = iKbd.toTelegramInline()
} } else if kbd != nil {
msg.ReplyMarkup = kbd.toTelegramReply()
msg = apix.NewMessage(id, ">") if _, err := c.B.Send(msg); err != nil {
// Checking if we need to resend the keyboard. return err
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
} }
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 // Screen text and reply keyboard.
if _, err := c.B.Send(msg); err != nil { txt = ""
return err 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 // The type represents current state of
// user interaction per each of them. // user interaction per each of them.
type Session struct { type Session struct {
// Id of the chat of the user.
Id SessionId Id SessionId
// Current screen identifier. // Custom value for each user.
CurrentScreenId ScreenId V any
// ID of the previous screen.
PreviousScreenId ScreenId
// The currently showed on display keyboard inside Action.
KeyboardId KeyboardId
V any
} }
// Return new empty session with specified user ID. // 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 // The type represents map of sessions using
// as key. // as key.
type SessionMap map[SessionId]*Session type SessionMap map[SessionId]*Session