Great refactoring. Should put more of the handling into the Actions.

This commit is contained in:
Andrey Parhomenko 2023-09-08 17:37:32 +03:00
parent b2748a8cee
commit 5b00189ea2
9 changed files with 227 additions and 166 deletions

View file

@ -20,7 +20,7 @@ var (
startScreenButton = tg.NewButton("🏠 To the start screen"). startScreenButton = tg.NewButton("🏠 To the start screen").
ScreenChange("start") ScreenChange("start")
incDecKeyboard = tg.NewKeyboard("").Row( incDecKeyboard = tg.NewInline().Row(
tg.NewButton("+").ActionFunc(func(c *tg.Context) { tg.NewButton("+").ActionFunc(func(c *tg.Context) {
d := c.Session.Value.(*UserData) d := c.Session.Value.(*UserData)
d.Counter++ d.Counter++
@ -35,7 +35,7 @@ var (
startScreenButton, startScreenButton,
) )
navKeyboard = tg.NewKeyboard("Choose your interest"). navKeyboard = tg.NewReply().
WithOneTime(true). WithOneTime(true).
Row( Row(
tg.NewButton("Inc/Dec").ScreenChange("inc/dec"), tg.NewButton("Inc/Dec").ScreenChange("inc/dec"),
@ -46,7 +46,7 @@ var (
tg.NewButton("Send location").ScreenChange("send-location"), tg.NewButton("Send location").ScreenChange("send-location"),
) )
sendLocationKeyboard = tg.NewKeyboard("Press the button to send your location"). sendLocationKeyboard = tg.NewReply().
Row( Row(
tg.NewButton("Send location"). tg.NewButton("Send location").
WithSendLocation(true). WithSendLocation(true).
@ -64,10 +64,10 @@ var (
l.Heading, l.Heading,
) )
} else { } else {
_, err = c.Send("Somehow wrong location was sent") _, err = c.Sendf("Somehow wrong location was sent")
} }
if err != nil { if err != nil {
c.Send(err) c.Sendf("%q", err)
} }
}), }),
).Row( ).Row(
@ -75,7 +75,7 @@ var (
) )
// The keyboard to return to the start screen. // The keyboard to return to the start screen.
navToStartKeyboard = tg.NewKeyboard("").Row( navToStartKeyboard = tg.NewReply().Row(
startScreenButton, startScreenButton,
) )
) )
@ -87,7 +87,7 @@ var beh = tg.NewBehaviour().
}). // On any message update before the bot created session. }). // On any message update before the bot created session.
WithPreStartFunc(func(c *tg.Context){ WithPreStartFunc(func(c *tg.Context){
c.Send("Please, use the /start command to start the bot") c.Sendf("Please, use the /start command to start the bot")
}).WithScreens( }).WithScreens(
tg.NewScreen("start"). tg.NewScreen("start").
WithText( WithText(
@ -96,10 +96,10 @@ var beh = tg.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!",
).WithKeyboard(navKeyboard). ).WithReply(navKeyboard).
// The inline keyboard with link to GitHub page. // The inline keyboard with link to GitHub page.
WithIKeyboard( WithInline(
tg.NewKeyboard("istart").Row( tg.NewInline().Row(
tg.NewButton("GoT Github page"). tg.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"), WithUrl("https://github.com/mojosa-software/got"),
), ),
@ -112,7 +112,7 @@ var beh = tg.NewBehaviour().
"by saving the counter for each of users "+ "by saving the counter for each of users "+
"separately. ", "separately. ",
). ).
WithKeyboard(incDecKeyboard). WithReply(&tg.ReplyKeyboard{Keyboard: incDecKeyboard.Keyboard}).
// The function will be called when reaching the screen. // The function will be called when reaching the screen.
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
d := c.Session.Value.(*UserData) d := c.Session.Value.(*UserData)
@ -121,19 +121,19 @@ var beh = tg.NewBehaviour().
tg.NewScreen("upper-case"). tg.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").
WithKeyboard(navToStartKeyboard). WithReply(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToUpper)), ActionFunc(mutateMessage(strings.ToUpper)),
tg.NewScreen("lower-case"). tg.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").
WithKeyboard(navToStartKeyboard). WithReply(navToStartKeyboard).
ActionFunc(mutateMessage(strings.ToLower)), ActionFunc(mutateMessage(strings.ToLower)),
tg.NewScreen("send-location"). tg.NewScreen("send-location").
WithText("Send your location and I will tell where you are!"). WithText("Send your location and I will tell where you are!").
WithKeyboard(sendLocationKeyboard). WithReply(sendLocationKeyboard).
WithIKeyboard( WithInline(
tg.NewKeyboard("").Row( tg.NewInline().Row(
tg.NewButton("Check"). tg.NewButton("Check").
WithData("check"). WithData("check").
ActionFunc(func(a *tg.Context) { ActionFunc(func(a *tg.Context) {
@ -151,12 +151,12 @@ var beh = tg.NewBehaviour().
tg.NewCommand("hello"). tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back"). Desc("sends the 'Hello, World!' message back").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
c.Send("Hello, World!") c.Sendf("Hello, World!")
}), }),
tg.NewCommand("read"). tg.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
c.Send("Type some text:") c.Sendf("Type some text:")
msg, err := c.ReadTextMessage() msg, err := c.ReadTextMessage()
if err != nil { if err != nil {
return return

View file

@ -8,7 +8,6 @@ type Behaviour struct {
PreStart *action PreStart *action
Init *action Init *action
Screens ScreenMap Screens ScreenMap
Keyboards KeyboardMap
Commands CommandMap Commands CommandMap
} }
@ -16,7 +15,6 @@ type Behaviour struct {
func NewBehaviour() *Behaviour { func NewBehaviour() *Behaviour {
return &Behaviour{ return &Behaviour{
Screens: make(ScreenMap), Screens: make(ScreenMap),
Keyboards: make(KeyboardMap),
Commands: make(CommandMap), Commands: make(CommandMap),
} }
} }

View file

@ -3,7 +3,7 @@ package tg
import ( import (
"errors" "errors"
"fmt" //"fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
@ -25,6 +25,7 @@ type Bot struct {
sessions SessionMap sessions SessionMap
groupSessions GroupSessionMap groupSessions GroupSessionMap
value any value any
} }
// Return the new bot with empty sessions and behaviour. // Return the new bot with empty sessions and behaviour.
@ -58,25 +59,36 @@ func (bot *Bot) Debug(debug bool) *Bot {
} }
func (bot *Bot) Send( func (bot *Bot) Send(
sid SessionId, v any, sid SessionId, v Sendable,
) (*Message, error) { ) (*Message, error) {
sendable, ok := v.(Sendable) config, err := v.SendConfig(sid, bot)
if !ok { if err != nil {
cid := sid.ToApi() return nil, err
str := tgbotapi.NewMessage(
cid, fmt.Sprint(v),
)
msg, err := bot.Api.Send(str)
return &msg, err
} }
return sendable.Send(sid, bot) msg, err := bot.Api.Send(config.ToApi())
if err != nil {
return nil, err
}
return &msg, nil
} }
func (bot *Bot) Render( func (bot *Bot) Render(
sid SessionId, r Renderable, sid SessionId, r Renderable,
) ([]*Message, error) { ) ([]*Message, error) {
return r.Render(sid, bot) configs, err := r.Render(sid, bot)
if err != nil {
return []*Message{}, err
}
messages := []*Message{}
for _, config := range configs {
msg, err := bot.Api.Send(config.ToApi())
if err != nil {
return messages, err
}
messages = append(messages, &msg)
}
return messages, nil
} }
func (bot *Bot) GetSession( func (bot *Bot) GetSession(

View file

@ -10,6 +10,7 @@ import (
"github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type PhotoConfig = tgbotapi.PhotoConfig
type FileType int type FileType int
const ( const (
@ -71,22 +72,22 @@ func (f *File) UploadData() (string, io.Reader, error) {
func (f *File) SendData() string { func (f *File) SendData() string {
return "" return ""
} }
func (f *File) Send( func (f *File) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*Message, error) { ) (*SendConfig, error) {
var chattable tgbotapi.Chattable var config SendConfig
cid := sid.ToApi() cid := sid.ToApi()
switch f.Type() { switch f.Type() {
case ImageFileType: case ImageFileType:
photo := tgbotapi.NewPhoto(cid, f) photo := tgbotapi.NewPhoto(cid, f)
photo.Caption = f.caption photo.Caption = f.caption
chattable = photo
config.Image = &photo
default: default:
return nil, UnknownFileTypeErr return nil, UnknownFileTypeErr
} }
msg, err := bot.Api.Send(chattable)
return &msg, err return &config, nil
} }

View file

@ -1,57 +1,48 @@
package tg package tg
import ( import (
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
/* // The general keyboard type used both in Reply and Inline.
var otherKeyboard = tgbotapi.NewReplyKeyboard(
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("a"),
tgbotapi.NewKeyboardButton("b"),
tgbotapi.NewKeyboardButton("c"),
),
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("d"),
tgbotapi.NewKeyboardButton("e"),
tgbotapi.NewKeyboardButton("f"),
),
)*/
type KeyboardId string
// The type represents reply keyboard which
// is supposed to be showed on a Screen.
type Keyboard struct { type Keyboard struct {
// Text to be displayed with the keyboard.
Text string
// Rows to be displayed once the
// keyboard is sent.
Rows []ButtonRow Rows []ButtonRow
}
type ReplyKeyboard struct {
Keyboard
// If true will be removed after one press.
OneTime bool OneTime bool
Inline bool // If true will remove the keyboard on send.
Remove bool
} }
type KeyboardMap map[KeyboardId]*Keyboard // The keyboard to be emdedded into the messages.
type InlineKeyboard struct {
// Return the new reply keyboard with rows as specified. Keyboard
func NewKeyboard(text string) *Keyboard {
return &Keyboard{
Text: text,
}
} }
func (kbd *Keyboard) TelegramMarkup() any { func NewInline() *InlineKeyboard {
if kbd.Inline { ret := &InlineKeyboard{}
return kbd.toTelegramInline() return ret
} }
return kbd.toTelegramReply() func NewReply() *ReplyKeyboard {
ret := &ReplyKeyboard {}
return ret
} }
// 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 *InlineKeyboard) Row(btns ...*Button) *InlineKeyboard {
// For empty row. We do not need that.
if len(btns) < 1 {
return kbd
}
kbd.Rows = append(kbd.Rows, btns)
return kbd
}
// Adds a new button row to the current keyboard.
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
// For empty row. We do not need that. // For empty row. We do not need that.
if len(btns) < 1 { if len(btns) < 1 {
return kbd return kbd
@ -61,10 +52,15 @@ func (kbd *Keyboard) Row(btns ...*Button) *Keyboard {
} }
// Convert the Keyboard to the Telegram API type of reply keyboard. // Convert the Keyboard to the Telegram API type of reply keyboard.
func (kbd *Keyboard) toTelegramReply() apix.ReplyKeyboardMarkup { func (kbd *ReplyKeyboard) ToApi() any {
rows := [][]apix.KeyboardButton{} // Shades everything.
if kbd.Remove {
return tgbotapi.NewRemoveKeyboard(true)
}
rows := [][]tgbotapi.KeyboardButton{}
for _, row := range kbd.Rows { for _, row := range kbd.Rows {
buttons := []apix.KeyboardButton{} buttons := []tgbotapi.KeyboardButton{}
for _, button := range row { for _, button := range row {
buttons = append(buttons, button.ToTelegram()) buttons = append(buttons, button.ToTelegram())
} }
@ -72,37 +68,37 @@ func (kbd *Keyboard) toTelegramReply() apix.ReplyKeyboardMarkup {
} }
if kbd.OneTime { if kbd.OneTime {
return apix.NewOneTimeReplyKeyboard(rows...) return tgbotapi.NewOneTimeReplyKeyboard(rows...)
} }
return apix.NewReplyKeyboard(rows...) return tgbotapi.NewReplyKeyboard(rows...)
} }
func (kbd *Keyboard) toTelegramInline() apix.InlineKeyboardMarkup { func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup {
rows := [][]apix.InlineKeyboardButton{} rows := [][]tgbotapi.InlineKeyboardButton{}
for _, row := range kbd.Rows { for _, row := range kbd.Rows {
buttons := []apix.InlineKeyboardButton{} buttons := []tgbotapi.InlineKeyboardButton{}
for _, button := range row { for _, button := range row {
buttons = append(buttons, button.ToTelegramInline()) buttons = append(buttons, button.ToTelegramInline())
} }
rows = append(rows, buttons) rows = append(rows, buttons)
} }
return apix.NewInlineKeyboardMarkup(rows...) return tgbotapi.NewInlineKeyboardMarkup(rows...)
} }
func (kbd *Keyboard) WithOneTime(oneTime bool) *Keyboard { func (kbd *ReplyKeyboard) WithRemove(remove bool) *ReplyKeyboard {
kbd.Remove = remove
return kbd
}
func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
kbd.OneTime = oneTime kbd.OneTime = oneTime
return kbd 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)
for _, vi := range kbd.Rows { for _, vi := range kbd.Rows {
for _, vj := range vi { for _, vj := range vi {

51
tg/message.go Normal file
View file

@ -0,0 +1,51 @@
package tg
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type MessageConfig struct {
To SessionId
ReplyTo MessageId
Text string
Inline *InlineKeyboard
Reply *ReplyKeyboard
}
func NewMessage(to SessionId, text string) *MessageConfig {
ret := &MessageConfig{}
ret.To = to
ret.Text = text
return ret
}
func (config *MessageConfig) WithInline(
inline *InlineKeyboard,
) *MessageConfig {
config.Inline = inline
return config
}
func (config *MessageConfig) WithReply(
reply *ReplyKeyboard,
) *MessageConfig {
config.Reply = reply
return config
}
func (config *MessageConfig) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig, error) {
var ret SendConfig
msg := tgbotapi.NewMessage(config.To.ToApi(), config.Text)
if config.Inline != nil {
msg.ReplyMarkup = config.Inline.ToApi()
}
// Reply shades the inline.
if config.Reply != nil {
msg.ReplyMarkup = config.Reply.ToApi()
}
ret.Message = &msg
return &ret, nil
}

View file

@ -61,7 +61,7 @@ func (c *context) handleUpdateChan(updates chan *Update) {
} }
} else { } else {
// Simple messages handling. // Simple messages handling.
kbd := screen.Keyboard kbd := screen.Reply
if kbd == nil { if kbd == nil {
if c.readingUpdate { if c.readingUpdate {
c.updates <- u c.updates <- u
@ -102,7 +102,7 @@ func (c *context) handleUpdateChan(updates chan *Update) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
kbd := screen.InlineKeyboard kbd := screen.Inline
if kbd == nil { if kbd == nil {
if c.readingUpdate { if c.readingUpdate {
c.updates <- u c.updates <- u
@ -160,14 +160,16 @@ func (c *context) ReadTextMessage() (string, error) {
return u.Message.Text, nil return u.Message.Text, nil
} }
// Sends to the user specified text. // Sends to the Sendable object.
func (c *context) Send(v any) (*Message, error) { func (c *context) Send(v Sendable) (*Message, error) {
return c.Bot.Send(c.Session.Id, v) return c.Bot.Send(c.Session.Id, v)
} }
// Sends the formatted with fmt.Sprintf message to the user. // Sends the formatted with fmt.Sprintf message to the user.
func (c *context) Sendf(format string, v ...any) (*Message, error) { func (c *context) Sendf(format string, v ...any) (*Message, error) {
msg, err := c.Send(fmt.Sprintf(format, v...)) msg, err := c.Send(NewMessage(
c.Session.Id, fmt.Sprintf(format, v...),
))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -224,7 +226,7 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
screen := c.Bot.behaviour.Screens[screenId] screen := c.Bot.behaviour.Screens[screenId]
c.prevScreen = c.curScreen c.prevScreen = c.curScreen
c.curScreen = screen c.curScreen = screen
screen.Render(c.Session.Id, c.Bot) c.Bot.Render(c.Session.Id, screen)
if screen.Action != nil { if screen.Action != nil {
c.run(screen.Action, c.Update) c.run(screen.Action, c.Update)
} }

View file

@ -15,9 +15,9 @@ type Screen struct {
// reached. // reached.
Text string Text string
// The keyboard to be sent in the message part. // The keyboard to be sent in the message part.
InlineKeyboard *Keyboard Inline *InlineKeyboard
// Keyboard to be displayed on the screen. // Keyboard to be displayed on the screen.
Keyboard *Keyboard Reply *ReplyKeyboard
// Action called on the reaching the screen. // Action called on the reaching the screen.
Action *action Action *action
} }
@ -38,17 +38,13 @@ func (s *Screen) WithText(text string) *Screen {
return s return s
} }
func (s *Screen) WithInlineKeyboard(ikbd *Keyboard) *Screen { func (s *Screen) WithInline(ikbd *InlineKeyboard) *Screen {
s.InlineKeyboard = ikbd s.Inline= ikbd
return s return s
} }
func (s *Screen) WithIKeyboard(ikbd *Keyboard) *Screen { func (s *Screen) WithReply(kbd *ReplyKeyboard) *Screen {
return s.WithInlineKeyboard(ikbd) s.Reply = kbd
}
func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen {
s.Keyboard = kbd
return s return s
} }
@ -61,81 +57,54 @@ func (s *Screen) ActionFunc(a ActionFunc) *Screen {
return s.WithAction(a) return s.WithAction(a)
} }
// Renders output of the screen only to the side of the user.
func (s *Screen) Render( func (s *Screen) Render(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) ([]*Message, error) { ) ([]*SendConfig, error) {
cid := sid.ToApi() cid := sid.ToApi()
kbd := s.Keyboard reply := s.Reply
iKbd := s.InlineKeyboard inline := s.Inline
ret := []*SendConfig{}
var ch [2]tgbotapi.Chattable
var txt string var txt string
msgs := []*Message{}
// Screen text and inline keyboard. // Screen text and inline keyboard.
if s.Text != "" { if s.Text != "" {
txt = s.Text txt = s.Text
} else if iKbd != nil { } else if inline != nil {
if iKbd.Text != "" { // Default to send the keyboard.
txt = iKbd.Text txt = ">"
} else {
// Default to send the keyboard.
txt = ">"
}
} }
if txt != "" { if txt != "" {
msgConfig := tgbotapi.NewMessage(cid, txt) msgConfig := tgbotapi.NewMessage(cid, txt)
if iKbd != nil { if inline != nil {
msgConfig.ReplyMarkup = iKbd.toTelegramInline() msgConfig.ReplyMarkup = inline.ToApi()
} else if kbd != nil { } else if reply != nil {
msgConfig.ReplyMarkup = kbd.toTelegramReply() msgConfig.ReplyMarkup = reply.ToApi()
msg, err := bot.Api.Send(msgConfig) ret = append(ret, &SendConfig{Message: &msgConfig})
if err != nil { return ret, nil
return msgs, err
}
msgs = append(msgs, &msg)
return msgs, nil
} else { } else {
msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) msgConfig.ReplyMarkup = NewReply().
msg, err := bot.Api.Send(msgConfig) WithRemove(true).
if err != nil { ToApi()
return msgs, err ret = append(ret, &SendConfig{Message: &msgConfig})
} return ret, nil
msgs = append(msgs, &msg)
return msgs, nil
} }
ch[0] = msgConfig ret = append(ret, &SendConfig{Message: &msgConfig})
} }
// Screen text and reply keyboard. // Screen text and reply keyboard.
txt = "" if reply != nil {
if kbd != nil { msgConfig := tgbotapi.NewMessage(cid, ">")
if kbd.Text != "" { msgConfig.ReplyMarkup = reply.ToApi()
txt = kbd.Text ret = append(ret, &SendConfig{
} else { Message: &msgConfig,
txt = ">" })
}
msgConfig := tgbotapi.NewMessage(cid, txt)
msgConfig.ReplyMarkup = kbd.toTelegramReply()
ch[1] = msgConfig
} else { } else {
// Removing keyboard if there is none. // Removing keyboard if there is none.
msgConfig := tgbotapi.NewMessage(cid, ">") msgConfig := tgbotapi.NewMessage(cid, ">")
msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) msgConfig.ReplyMarkup = NewReply().
ch[1] = msgConfig WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
} }
for _, m := range ch { return ret, nil
if m != nil {
msg, err := bot.Api.Send(m)
if err != nil {
return msgs, err
}
msgs = append(msgs, &msg)
}
}
return msgs, nil
} }

View file

@ -1,11 +1,43 @@
package tg package tg
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type MessageId int64
type Image any
// Implementing the interface lets the // Implementing the interface lets the
// value to be sent. // value to be sent.
type Sendable interface { type Sendable interface {
Send(SessionId, *Bot) (*Message, error) SendConfig(SessionId, *Bot) (*SendConfig, error)
} }
type Renderable interface { type Renderable interface {
Render(SessionId, *Bot) ([]*Message, error) Render(SessionId, *Bot) ([]*SendConfig, error)
} }
// The type is used as an endpoint to send messages
// via bot.Send .
type SendConfig struct {
// Simple message with text.
// to add text use lower image
// or see the ParseMode for tgbotapi .
Message *tgbotapi.MessageConfig
// The image to be sent.
Image *tgbotapi.PhotoConfig
}
// Convert to the bot.Api.Send format.
func (config *SendConfig) ToApi() tgbotapi.Chattable {
if config.Message != nil {
return *config.Message
}
if config.Image != nil {
return *config.Image
}
return nil
}